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::{InputSlot, JournalStage, JournalStageRun};
20
21use super::auth::authenticate_with_primary;
22use super::receiver_transport::{
23 ControlFrameSource, ReceiverTransport, SessionExit, streaming_loop,
24};
25use super::{
26 AfterSession, MAX_BACKOFF, ReplicaPipelineHandles, ResyncDecision,
27 build_replica_pipeline_with_threads, handle_resync_verdict, handle_session_exit,
28 recover_replica_state, sleep_checking_flags, take_pipeline_for_promotion,
29 teardown_replica_pipeline,
30};
31use melin_transport_core::replication::protocol::{
32 Ack, Handshake, MAX_CONTROL_FRAME, MAX_DATA_FRAME, PrimaryMessage, decode_primary_message,
33 encode_ack, encode_handshake, read_frame,
34};
35
36#[inline]
41fn arm_tcp_quickack(fd: RawFd) {
42 let on: libc::c_int = 1;
43 let rc = unsafe {
47 libc::setsockopt(
48 fd,
49 libc::IPPROTO_TCP,
50 libc::TCP_QUICKACK,
51 &on as *const libc::c_int as *const libc::c_void,
52 std::mem::size_of::<libc::c_int>() as libc::socklen_t,
53 )
54 };
55 if rc != 0 {
56 let _ = rc;
57 }
58}
59
60const TOKEN_RECV: u64 = 0;
65const TOKEN_SEND: u64 = 1;
66const TOKEN_PROVIDE: u64 = 2;
67
68const NUM_RECV_BUFFERS: u16 = 16;
70const RECV_BUF_SIZE: usize = MAX_DATA_FRAME;
71const RECV_BUF_GROUP_ID: u16 = 0;
72
73const IORING_CQE_F_BUFFER: u32 = 1 << 0;
75const IORING_CQE_F_MORE: u32 = 1 << 1;
76const IORING_CQE_BUFFER_SHIFT: u32 = 16;
77
78struct UringTransport {
84 ring: io_uring::IoUring,
85 tcp_fd: RawFd,
86 _recv_pool: Vec<u8>,
90 pool_ptr: *mut u8,
91 multishot_active: bool,
92 connected: bool,
93 ack_buf: Vec<u8>,
94 ack_offset: usize,
95 ack_in_flight: bool,
96 pending_ack: Option<Ack>,
106}
107
108impl UringTransport {
109 fn new(tcp_stream: &TcpStream) -> io::Result<Self> {
110 use io_uring::{IoUring, opcode, types};
111 use std::os::unix::io::AsRawFd;
112
113 let tcp_fd = tcp_stream.as_raw_fd();
114
115 let mut ring: IoUring = IoUring::builder()
116 .setup_single_issuer()
117 .build(64)
118 .map_err(|e| io::Error::other(format!("io_uring init failed: {e}")))?;
119
120 ring.submitter()
121 .register_files(&[tcp_fd])
122 .map_err(|e| io::Error::other(format!("io_uring register_files failed: {e}")))?;
123
124 arm_tcp_quickack(tcp_fd);
125
126 {
128 let mut cpuset: libc::cpu_set_t = unsafe { std::mem::zeroed() };
129 unsafe { libc::CPU_SET(0, &mut cpuset) };
130 let _ = ring.submitter().register_iowq_aff(&cpuset);
131 }
132
133 let mut recv_pool: Vec<u8> = vec![0u8; NUM_RECV_BUFFERS as usize * RECV_BUF_SIZE];
134 let pool_ptr = recv_pool.as_mut_ptr();
135
136 {
138 let sqe = opcode::ProvideBuffers::new(
139 pool_ptr,
140 RECV_BUF_SIZE as i32,
141 NUM_RECV_BUFFERS,
142 RECV_BUF_GROUP_ID,
143 0,
144 )
145 .build()
146 .user_data(TOKEN_PROVIDE);
147 unsafe { ring.submission().push(&sqe).expect("SQ full") };
148 ring.submit_and_wait(1)?;
149 let cqe = ring
150 .completion()
151 .next()
152 .ok_or_else(|| io::Error::other("no cqe after ProvideBuffers"))?;
153 if cqe.result() < 0 {
154 return Err(io::Error::other(format!(
155 "ProvideBuffers failed: {}",
156 cqe.result()
157 )));
158 }
159 }
160
161 let sqe = opcode::RecvMulti::new(types::Fd(tcp_fd), RECV_BUF_GROUP_ID)
163 .build()
164 .user_data(TOKEN_RECV);
165 unsafe { ring.submission().push(&sqe).expect("SQ full") };
166
167 Ok(UringTransport {
168 ring,
169 tcp_fd,
170 _recv_pool: recv_pool,
171 pool_ptr,
172 multishot_active: true,
173 connected: true,
174 ack_buf: Vec::with_capacity(64),
175 ack_offset: 0,
176 ack_in_flight: false,
177 pending_ack: None,
178 })
179 }
180
181 fn push_ack_send(&mut self, ack: &Ack) {
187 use io_uring::{opcode, types};
188
189 debug_assert!(!self.ack_in_flight);
190 self.ack_buf.clear();
191 encode_ack(ack, &mut self.ack_buf);
192 let sqe = opcode::Send::new(
193 types::Fixed(0),
194 self.ack_buf.as_ptr(),
195 self.ack_buf.len() as u32,
196 )
197 .build()
198 .user_data(TOKEN_SEND);
199 unsafe { self.ring.submission().push(&sqe).expect("SQ full") };
203 self.ack_in_flight = true;
204 self.ack_offset = 0;
205 }
206}
207
208impl ReceiverTransport for UringTransport {
209 fn poll_recv(&mut self, recv_buf: &mut Vec<u8>) -> io::Result<bool> {
210 use io_uring::{opcode, types};
211
212 if !self.connected {
213 return Err(io::Error::other("not connected"));
214 }
215
216 let pending = self.ring.submission().len();
219 if pending > 0 {
220 self.ring.submit()?;
221 }
222
223 let mut cqes: [(u64, i32, u32); 16] = [(0, 0, 0); 16];
225 let mut cqe_count = 0;
226 {
227 let mut cq = self.ring.completion();
228 while cqe_count < cqes.len() {
229 match cq.next() {
230 Some(cqe) => {
231 cqes[cqe_count] = (cqe.user_data(), cqe.result(), cqe.flags());
232 cqe_count += 1;
233 }
234 None => break,
235 }
236 }
237 }
238
239 let mut any_recv = false;
240 let mut submit_chained_ack = false;
244
245 for &(token, result, flags) in &cqes[..cqe_count] {
246 match token {
247 TOKEN_RECV => {
248 if (flags & IORING_CQE_F_MORE) == 0 {
249 self.multishot_active = false;
250 }
251 if result < 0 {
252 if result == -libc::ENOBUFS {
253 tracing::debug!("recv multishot ENOBUFS — pool exhausted");
254 continue;
255 }
256 self.connected = false;
257 return Err(io::Error::other(format!(
258 "primary disconnected (recv returned {result})"
259 )));
260 }
261 if result == 0 {
262 self.connected = false;
263 return Err(io::Error::other("primary disconnected (recv returned 0)"));
264 }
265 if (flags & IORING_CQE_F_BUFFER) == 0 {
266 self.connected = false;
267 return Err(io::Error::other("recv cqe missing F_BUFFER flag"));
268 }
269
270 arm_tcp_quickack(self.tcp_fd);
271 let n = result as usize;
272 let buf_id = (flags >> IORING_CQE_BUFFER_SHIFT) as usize;
273 let buf_ptr = unsafe { self.pool_ptr.add(buf_id * RECV_BUF_SIZE) };
276 let slice = unsafe { std::slice::from_raw_parts(buf_ptr, n) };
277 recv_buf.extend_from_slice(slice);
278 any_recv = true;
279
280 let provide_sqe = opcode::ProvideBuffers::new(
282 buf_ptr,
283 RECV_BUF_SIZE as i32,
284 1,
285 RECV_BUF_GROUP_ID,
286 buf_id as u16,
287 )
288 .build()
289 .user_data(TOKEN_PROVIDE);
290 unsafe { self.ring.submission().push(&provide_sqe).expect("SQ full") };
291 }
292
293 TOKEN_PROVIDE if result < 0 => {
294 tracing::debug!("ProvideBuffers re-provision failed: {result}");
295 }
296
297 TOKEN_SEND => {
298 if result < 0 {
299 self.connected = false;
300 return Err(io::Error::other(format!(
301 "ack send error (returned {result})"
302 )));
303 }
304 let sent = result as usize;
305 self.ack_offset += sent;
306 if self.ack_offset >= self.ack_buf.len() {
307 self.ack_buf.clear();
308 self.ack_offset = 0;
309 self.ack_in_flight = false;
310 if let Some(ack) = self.pending_ack.take() {
317 self.push_ack_send(&ack);
318 submit_chained_ack = true;
319 }
320 } else {
321 let sqe = opcode::Send::new(
323 types::Fixed(0),
324 self.ack_buf[self.ack_offset..].as_ptr(),
325 (self.ack_buf.len() - self.ack_offset) as u32,
326 )
327 .build()
328 .user_data(TOKEN_SEND);
329 unsafe { self.ring.submission().push(&sqe).expect("SQ full") };
330 }
331 }
332
333 _ => {}
334 }
335 }
336
337 if !self.multishot_active {
339 let sqe = opcode::RecvMulti::new(types::Fd(self.tcp_fd), RECV_BUF_GROUP_ID)
340 .build()
341 .user_data(TOKEN_RECV);
342 unsafe { self.ring.submission().push(&sqe).expect("SQ full") };
343 self.multishot_active = true;
344 }
345
346 if submit_chained_ack {
349 self.ring.submit()?;
350 }
351
352 Ok(any_recv)
353 }
354
355 fn send_ack(&mut self, ack: &Ack) -> io::Result<bool> {
356 if self.ack_in_flight {
357 self.pending_ack = Some(*ack);
361 return Ok(true);
362 }
363 self.push_ack_send(ack);
364 Ok(true)
365 }
366
367 fn ack_in_flight(&self) -> bool {
368 self.ack_in_flight || self.pending_ack.is_some()
373 }
374
375 fn is_connected(&mut self) -> bool {
376 self.connected
377 }
378}
379
380struct TcpFrameSource<'a> {
388 reader: &'a mut TcpStream,
389}
390
391impl ControlFrameSource for TcpFrameSource<'_> {
392 fn next_frame(
393 &mut self,
394 max_size: usize,
395 ) -> Result<Vec<u8>, Box<dyn std::error::Error + Send + Sync>> {
396 Ok(read_frame(self.reader, max_size)?)
397 }
398}
399
400pub type ReceiverResult<A, W> = Result<Option<(A, W)>, Box<dyn std::error::Error>>;
402
403#[allow(clippy::too_many_arguments)]
404pub fn run_receiver<A, W>(
405 primary_addr: SocketAddr,
406 journal_path: &std::path::Path,
407 signing_key: &ed25519_dalek::SigningKey,
408 shutdown: &AtomicBool,
409 promote: &AtomicBool,
410 snapshot_interval_ms: u64,
411 snapshot_path: std::path::PathBuf,
412 cores: crate::server::PipelineCores,
413 group_commit_delay: std::time::Duration,
414 pipeline_depth: usize,
415 busy_spin: bool,
416 factory: std::sync::Arc<dyn melin_app::app_factory::AppFactory<App = A>>,
417 fence_state: std::sync::Arc<melin_transport_core::fence::FenceState>,
418) -> ReceiverResult<A, W>
419where
420 A: Application + Send + 'static,
421 A::Event: Send + Sync + 'static,
422 A::Report: Send + 'static,
423 A::QueryResponse: Send + 'static,
424 W: JournalWrite<A::Event> + Send + 'static,
425 JournalStage<A::Event, W>: JournalStageRun<A::Event, Writer = W>,
426{
427 let (mut exchange, mut journal_writer, mut last_sequence, mut chain_hash) =
431 recover_replica_state::<A, W>(
432 journal_path,
433 &snapshot_path,
434 factory.as_ref(),
435 &fence_state,
436 )?;
437
438 let mut backoff = std::time::Duration::from_secs(1);
439
440 let mut divergence_resyncs: u32 = 0;
443
444 let mut send_buf = Vec::with_capacity(64);
445 let mut pipeline: Option<ReplicaPipelineHandles<A, W>> = None;
446
447 loop {
449 if let Some(p) = pipeline.as_ref() {
450 if let Some(ref lock) = p.chain_hash_lock {
459 let fsync_state = lock.load();
460 last_sequence = fsync_state.journal_seq.get();
461 chain_hash = fsync_state.chain_hash;
462 } else {
463 last_sequence = p.last_seq.load().get();
464 }
465 }
466
467 if shutdown.load(Ordering::Relaxed) {
468 if let Some(mut p) = pipeline.take() {
469 p.input_producer
470 .publish(InputSlot::<A::Event>::shutdown_sentinel());
471 let _ = teardown_replica_pipeline::<A, W>(p);
472 }
473 return Ok(None);
474 }
475 if promote.load(Ordering::Acquire) {
476 info!("promotion triggered while disconnected");
477 return take_pipeline_for_promotion(&mut pipeline, &mut exchange, &mut journal_writer);
478 }
479
480 info!(primary = %primary_addr, "connecting to primary as replica");
482
483 let stream = match TcpStream::connect(primary_addr) {
484 Ok(s) => s,
485 Err(e) => {
486 warn!(
487 error = %e,
488 backoff_secs = backoff.as_secs(),
489 "failed to connect to primary — retrying"
490 );
491 sleep_checking_flags(backoff, shutdown, promote);
492 if shutdown.load(Ordering::Relaxed) {
493 return Ok(None);
494 }
495 if promote.load(Ordering::Acquire) {
496 info!("promotion triggered during reconnect backoff");
497 return take_pipeline_for_promotion(
498 &mut pipeline,
499 &mut exchange,
500 &mut journal_writer,
501 );
502 }
503 backoff = (backoff * 2).min(MAX_BACKOFF);
504 continue;
505 }
506 };
507 if let Err(e) = stream.set_nodelay(true) {
508 warn!(error = %e, "failed to set TCP_NODELAY on replica receive socket");
509 }
510 {
511 use std::os::unix::io::AsRawFd;
512 let busy_poll: libc::c_int = 50;
513 let rc = unsafe {
514 libc::setsockopt(
515 stream.as_raw_fd(),
516 libc::SOL_SOCKET,
517 libc::SO_BUSY_POLL,
518 &busy_poll as *const libc::c_int as *const libc::c_void,
519 std::mem::size_of::<libc::c_int>() as libc::socklen_t,
520 )
521 };
522 if rc != 0 {
523 let err = std::io::Error::last_os_error();
524 warn!(error = %err, "failed to set SO_BUSY_POLL on replica receive socket");
525 }
526 }
527 stream.set_read_timeout(Some(std::time::Duration::from_secs(5)))?;
528
529 let mut reader = stream.try_clone()?;
530 let mut tcp_writer = stream;
531
532 if let Err(e) = authenticate_with_primary(&mut reader, signing_key) {
535 warn!(error = %e, "authentication failed — retrying");
536 backoff = (backoff * 2).min(MAX_BACKOFF);
537 continue;
538 }
539 info!("authenticated with primary");
540
541 let handshake = Handshake {
546 last_sequence,
547 chain_hash,
548 epoch: fence_state.epoch(),
549 };
550 send_buf.clear();
551 encode_handshake(&handshake, &mut send_buf);
552 tcp_writer.write_all(&send_buf)?;
553 tcp_writer.flush()?;
554 send_buf.clear();
555
556 let response_frame = read_frame(&mut reader, MAX_CONTROL_FRAME)?;
563 let response = decode_primary_message(&response_frame)?;
564 let (stream_lineage, session_start) = match response {
565 PrimaryMessage::StreamStart {
566 start_sequence,
567 segment_start_sequence,
568 anchor_hash,
569 epoch,
570 } => {
571 let our_epoch = fence_state.epoch();
579 if fence_state.refuses_primary(epoch) {
580 warn!(
581 primary_epoch = epoch,
582 our_epoch,
583 "primary is behind our fencing epoch — refusing to follow stale primary"
584 );
585 backoff = (backoff * 2).min(MAX_BACKOFF);
586 sleep_checking_flags(backoff, shutdown, promote);
587 continue;
588 }
589 fence_state.observe_epoch(epoch);
592 info!(start_sequence, epoch, "streaming started");
593 ((segment_start_sequence, anchor_hash), last_sequence)
594 }
595 ref resync @ (PrimaryMessage::NeedSnapshot | PrimaryMessage::HashMismatch) => {
596 let divergent = matches!(resync, PrimaryMessage::HashMismatch);
597 let decision = handle_resync_verdict(
601 divergent,
602 &mut TcpFrameSource {
603 reader: &mut reader,
604 },
605 &mut pipeline,
606 &mut exchange,
607 &mut journal_writer,
608 journal_path,
609 &snapshot_path,
610 &fence_state,
611 &mut last_sequence,
612 &mut chain_hash,
613 )
614 .map_err(|e| -> Box<dyn std::error::Error> { e })?;
615 match decision {
616 ResyncDecision::Ready {
617 segment_start_sequence,
618 anchor_hash,
619 resume_sequence,
620 } => ((segment_start_sequence, anchor_hash), resume_sequence),
621 ResyncDecision::Retry => {
622 sleep_checking_flags(backoff, shutdown, promote);
623 backoff = (backoff * 2).min(MAX_BACKOFF);
624 continue;
625 }
626 }
627 }
628 _ => {
629 return Err(format!("unexpected response: {response:?}").into());
630 }
631 };
632
633 if pipeline.is_none() && journal_writer.is_none() {
641 let (lineage_start, lineage_anchor) = stream_lineage;
642 let writer = W::create_continuing(journal_path, lineage_start, lineage_anchor)?;
643 let mut fresh = factory.empty();
644 factory.apply_operator_policy(&mut fresh);
645 exchange = Some(fresh);
646 journal_writer = Some(writer);
647 }
648
649 if pipeline.is_none() {
651 let cur_exchange = exchange.take().expect("exchange initialized");
652 let cur_writer = journal_writer.take().expect("journal_writer initialized");
653 pipeline = Some(build_replica_pipeline_with_threads::<A, W>(
654 cur_exchange,
655 cur_writer,
656 cores,
657 snapshot_interval_ms,
658 snapshot_path.clone(),
659 group_commit_delay,
660 busy_spin,
661 Arc::clone(&fence_state),
662 )?);
663 }
664
665 let result = {
667 let p = pipeline.as_mut().expect("pipeline must exist by here");
668 let input_producer = &mut p.input_producer;
669 let journal_cursor = p.journal_cursor.as_ref();
670 let stream_marks = &p.stream_marks;
671 let journal_failed = &p.journal_failed;
672 std::thread::scope(|s| {
673 let handle = std::thread::Builder::new()
674 .name("replica-receiver".into())
675 .spawn_scoped(s, || {
676 melin_app::affinity::pin_thread("replica-receiver", cores.reader);
677 let mut transport = match UringTransport::new(&tcp_writer) {
678 Ok(t) => t,
679 Err(e) => {
680 tracing::error!(error = %e, "UringTransport init failed");
681 return super::receiver_transport::StreamingResult {
682 exit: SessionExit::Disconnected,
683 received_data: false,
684 };
685 }
686 };
687 streaming_loop::<UringTransport, A::Event>(
688 &mut transport,
689 input_producer,
690 journal_cursor,
691 shutdown,
692 promote,
693 pipeline_depth,
694 busy_spin,
695 session_start,
696 Vec::with_capacity(MAX_DATA_FRAME + 4),
697 None,
698 stream_marks,
699 journal_failed,
700 )
701 })
702 .expect("spawn replica-receiver thread");
703 handle.join().expect("replica-receiver thread panicked")
704 })
705 };
706
707 match handle_session_exit(
708 result,
709 &mut pipeline,
710 &mut divergence_resyncs,
711 &mut backoff,
712 last_sequence,
713 journal_path,
714 &snapshot_path,
715 factory.as_ref(),
716 &fence_state,
717 shutdown,
718 promote,
719 || {},
723 ) {
724 AfterSession::Return(r) => return r,
725 AfterSession::Resync {
726 exchange: ex,
727 journal_writer: wr,
728 last_sequence: seq,
729 chain_hash: hash,
730 } => {
731 exchange = ex;
732 journal_writer = wr;
733 last_sequence = seq;
734 chain_hash = hash;
735 continue;
736 }
737 AfterSession::Reconnect => {}
738 }
739 }
740}
741
742#[cfg(test)]
747mod tests {
748 use super::*;
749 use std::io::Read;
750 use std::net::TcpListener;
751 use std::time::{Duration, Instant};
752
753 use melin_transport_core::replication::protocol::{ReplicaMessage, decode_replica_message};
754
755 fn socket_pair() -> (TcpStream, TcpStream) {
757 let listener = TcpListener::bind("127.0.0.1:0").unwrap();
758 let addr = listener.local_addr().unwrap();
759 let client = TcpStream::connect(addr).unwrap();
760 let (server, _) = listener.accept().unwrap();
761 (client, server)
762 }
763
764 fn ack(seq: u64) -> Ack {
765 Ack {
766 acked_sequence: seq,
767 in_memory_sequence: seq,
768 }
769 }
770
771 fn flush_acks(transport: &mut UringTransport) {
773 let mut recv_buf = Vec::new();
774 let deadline = Instant::now() + Duration::from_secs(5);
775 while transport.ack_in_flight() {
776 transport.poll_recv(&mut recv_buf).unwrap();
777 assert!(Instant::now() < deadline, "ack send never completed");
778 std::hint::spin_loop();
779 }
780 }
781
782 fn read_acks(peer: &mut TcpStream, expect: usize) -> Vec<Ack> {
784 peer.set_read_timeout(Some(Duration::from_secs(5))).unwrap();
785 let mut buf = Vec::new();
786 let mut chunk = [0u8; 256];
787 let mut acks = Vec::new();
788 while acks.len() < expect {
789 let n = peer.read(&mut chunk).expect("peer read");
790 assert!(n > 0, "peer closed before all acks arrived");
791 buf.extend_from_slice(&chunk[..n]);
792 loop {
794 if buf.len() < 4 {
795 break;
796 }
797 let len = u32::from_le_bytes(buf[0..4].try_into().unwrap()) as usize;
798 if buf.len() < 4 + len {
799 break;
800 }
801 match decode_replica_message(&buf[4..4 + len]).expect("decodable frame") {
802 ReplicaMessage::Ack(a) => acks.push(a),
803 other => panic!("expected Ack frame, got {other:?}"),
804 }
805 buf.drain(..4 + len);
806 }
807 }
808 assert!(buf.is_empty(), "trailing bytes after expected acks");
809 acks
810 }
811
812 #[test]
813 fn coalesces_to_latest_ack_while_send_in_flight() {
814 let (client, mut peer) = socket_pair();
815 let mut transport = UringTransport::new(&client).unwrap();
816
817 assert!(transport.send_ack(&ack(1)).unwrap());
822 assert!(transport.ack_in_flight());
823 assert!(transport.send_ack(&ack(2)).unwrap());
824 assert!(transport.send_ack(&ack(3)).unwrap());
825
826 flush_acks(&mut transport);
827
828 let acks = read_acks(&mut peer, 2);
830 let seqs: Vec<u64> = acks.iter().map(|a| a.acked_sequence).collect();
831 assert_eq!(seqs, vec![1, 3]);
832 }
833
834 #[test]
835 fn sequential_acks_all_reach_the_wire_in_order() {
836 let (client, mut peer) = socket_pair();
837 let mut transport = UringTransport::new(&client).unwrap();
838
839 for seq in 1..=3 {
840 assert!(transport.send_ack(&ack(seq)).unwrap());
841 flush_acks(&mut transport);
842 }
843
844 let acks = read_acks(&mut peer, 3);
845 let seqs: Vec<u64> = acks.iter().map(|a| a.acked_sequence).collect();
846 assert_eq!(seqs, vec![1, 2, 3]);
847 }
848
849 #[cfg(all(feature = "hash-chain", not(feature = "no-persist")))]
855 mod divergence_resync {
856 use super::super::super::auth::authenticate_replica;
857 use super::*;
858 use melin_app::app_factory::AppFactory;
859 use melin_app::{AppEvent, Application, ApplyCtx, CodecError, RejectReason};
860 use melin_journal::{BufferedWriter, JournalEvent, JournalWrite};
861 use melin_transport_core::cursors::WireSeq;
862 use melin_transport_core::replication::catchup::{lineage_origin, snapshot_transfer_with};
863 use melin_transport_core::replication::protocol::{
864 encode_hash_mismatch, encode_rotate, encode_stream_start,
865 };
866 use std::sync::Arc;
867 use std::sync::atomic::AtomicBool;
868
869 #[derive(Debug, Clone, Copy, PartialEq)]
870 struct EvtAdd(u64);
871
872 impl AppEvent for EvtAdd {
873 fn encoded_size(&self) -> usize {
874 8
875 }
876 fn encode(&self, buf: &mut [u8]) -> usize {
877 buf[..8].copy_from_slice(&self.0.to_le_bytes());
878 8
879 }
880 fn decode(buf: &[u8]) -> Result<Self, CodecError> {
881 if buf.len() < 8 {
882 return Err(CodecError::Truncated);
883 }
884 Ok(EvtAdd(u64::from_le_bytes(buf[..8].try_into().expect("8"))))
885 }
886 fn is_query(&self) -> bool {
887 false
888 }
889 }
890
891 #[derive(Debug, Clone, Copy)]
892 struct Rpt;
893
894 struct App;
895
896 impl Application for App {
897 type Event = EvtAdd;
898 type Report = Rpt;
899 type QueryResponse = Rpt;
900 const APP_VERSION: u16 = 1;
901
902 fn apply(&mut self, _e: EvtAdd, _ctx: &ApplyCtx, _out: &mut Vec<Rpt>) -> Option<Rpt> {
903 None
904 }
905 fn tick(&mut self, _now_ns: u64, _out: &mut Vec<Rpt>) {}
906 fn check_request_seq(&mut self, _key_hash: u64, _seq: u64) -> bool {
907 true
908 }
909 fn build_reject(_e: &EvtAdd, _r: RejectReason) -> Rpt {
910 Rpt
911 }
912 fn snapshot<W: std::io::Write>(&self, _w: &mut W) -> std::io::Result<()> {
913 Ok(())
914 }
915 fn restore<R: std::io::Read>(_r: &mut R) -> std::io::Result<Self> {
916 Ok(App)
917 }
918 }
919
920 struct Factory;
921
922 impl AppFactory for Factory {
923 type App = App;
924 fn empty(&self) -> App {
925 App
926 }
927 fn prefault(&self, _app: &mut App) {}
928 }
929
930 fn read_replica_msg(stream: &mut TcpStream) -> ReplicaMessage {
932 let mut len_buf = [0u8; 4];
933 stream.read_exact(&mut len_buf).expect("frame length");
934 let len = u32::from_le_bytes(len_buf) as usize;
935 let mut payload = vec![0u8; len];
936 stream.read_exact(&mut payload).expect("frame payload");
937 decode_replica_message(&payload).expect("decodable replica frame")
938 }
939
940 fn wait_for_ack(stream: &mut TcpStream, seq: u64) {
942 loop {
943 if let ReplicaMessage::Ack(a) = read_replica_msg(stream)
944 && a.acked_sequence >= seq
945 {
946 return;
947 }
948 }
949 }
950
951 fn accept_within(listener: &std::net::TcpListener, secs: u64) -> TcpStream {
954 let deadline = Instant::now() + Duration::from_secs(secs);
955 loop {
956 match listener.accept() {
957 Ok((s, _)) => {
958 s.set_read_timeout(Some(Duration::from_secs(10)))
959 .expect("read timeout");
960 return s;
961 }
962 Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
963 assert!(Instant::now() < deadline, "replica never connected");
964 std::thread::sleep(Duration::from_millis(10));
965 }
966 Err(e) => panic!("accept failed: {e}"),
967 }
968 }
969 }
970
971 #[test]
980 fn mid_stream_divergence_resyncs_in_process() {
981 let dir = tempfile::tempdir().expect("tempdir");
982
983 let primary_journal = dir.path().join("primary.journal");
987 let mut w = BufferedWriter::<EvtAdd>::create(&primary_journal).expect("create");
988 let mut chain_at_4 = [0u8; 32];
989 for v in 1..=5u64 {
990 w.append(&JournalEvent::App(EvtAdd(v))).expect("append");
991 if v == 2 {
992 w.rotate_segment().expect("rotate");
993 }
994 if v == 4 {
995 chain_at_4 = w.chain_hash().expect("chain");
996 }
997 }
998 drop(w);
999 melin_transport_core::snapshot::save::<App>(
1000 &App,
1001 WireSeq::new(4),
1002 chain_at_4,
1003 0,
1004 &primary_journal.with_extension("snapshot"),
1005 )
1006 .expect("save snapshot");
1007 let (lineage_start, lineage_anchor) =
1008 lineage_origin(&primary_journal).expect("lineage");
1009
1010 let repl_key = ed25519_dalek::SigningKey::from_bytes(&[0xFC; 32]);
1012 let pub_b64 = base64::Engine::encode(
1013 &base64::engine::general_purpose::STANDARD,
1014 repl_key.verifying_key().to_bytes(),
1015 );
1016 let authorized_keys = melin_app::auth::AuthorizedKeys::parse(&format!(
1017 "replication {pub_b64} test-replica\n"
1018 ))
1019 .expect("parse keys");
1020
1021 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
1022 listener.set_nonblocking(true).expect("nonblocking");
1023 let addr = listener.local_addr().expect("addr");
1024
1025 let replica_journal = dir.path().join("replica.journal");
1027 let replica_snapshot = dir.path().join("replica.snapshot");
1028 let shutdown = Arc::new(AtomicBool::new(false));
1029 let promote = Arc::new(AtomicBool::new(false));
1030 let cores = crate::server::PipelineCores {
1031 journal: 0,
1033 matching: 0,
1034 response: 0,
1035 reader: 0,
1036 repl_sender: 0,
1037 event_publisher: 0,
1038 shadow: 0,
1039 repl_handler_0: 0,
1040 repl_handler_1: 0,
1041 };
1042 let replica = {
1043 let journal = replica_journal.clone();
1044 let shutdown = Arc::clone(&shutdown);
1045 let promote = Arc::clone(&promote);
1046 std::thread::spawn(move || -> Result<bool, String> {
1047 run_receiver::<App, BufferedWriter<EvtAdd>>(
1048 addr,
1049 &journal,
1050 &ed25519_dalek::SigningKey::from_bytes(&[0xFC; 32]),
1051 &shutdown,
1052 &promote,
1053 3_600_000,
1054 replica_snapshot,
1055 cores,
1056 Duration::ZERO,
1057 64,
1058 false,
1059 Arc::new(Factory),
1060 Arc::new(melin_transport_core::fence::FenceState::new(0)),
1061 )
1062 .map(|state| state.is_none())
1064 .map_err(|e| e.to_string())
1065 })
1066 };
1067
1068 let mut s1 = accept_within(&listener, 30);
1071 let mut s1r = s1.try_clone().expect("clone");
1072 authenticate_replica(&mut s1r, &authorized_keys).expect("auth 1");
1073 match read_replica_msg(&mut s1) {
1074 ReplicaMessage::Handshake(h) => {
1075 assert_eq!(h.last_sequence, 0, "fresh replica handshake")
1076 }
1077 other => panic!("expected Handshake, got {other:?}"),
1078 }
1079 let mut buf = Vec::new();
1080 encode_stream_start(0, lineage_start, lineage_anchor, 0, &mut buf);
1081 s1.write_all(&buf).expect("StreamStart 1");
1082 buf.clear();
1083 melin_transport_core::replication_wire::encode_input_batch(
1084 &[InputSlot::<EvtAdd> {
1085 connection_id: 0,
1086 key_hash: 0,
1087 request_seq: 0,
1088 sequence: 1,
1089 timestamp_ns: 1,
1090 event: JournalEvent::App(EvtAdd(1)),
1091 publish_ts: Default::default(),
1093 recv_ts: Default::default(),
1094 }],
1095 &mut buf,
1096 );
1097 s1.write_all(&buf).expect("InputBatch");
1098 wait_for_ack(&mut s1, 1);
1099
1100 buf.clear();
1103 encode_rotate(1, &[0xEE; 32], &mut buf);
1104 s1.write_all(&buf).expect("Rotate");
1105
1106 let mut s2 = accept_within(&listener, 30);
1109 let mut s2r = s2.try_clone().expect("clone");
1110 authenticate_replica(&mut s2r, &authorized_keys).expect("auth 2");
1111 match read_replica_msg(&mut s2) {
1112 ReplicaMessage::Handshake(h) => assert_eq!(
1113 h.last_sequence, 1,
1114 "post-divergence handshake must carry the recovered journal position"
1115 ),
1116 other => panic!("expected Handshake, got {other:?}"),
1117 }
1118 buf.clear();
1119 encode_hash_mismatch(&mut buf);
1120 s2.write_all(&buf).expect("HashMismatch");
1121 let transfer_shutdown = AtomicBool::new(false);
1122 let mut publish = |b: &[u8]| -> std::io::Result<()> {
1123 s2.write_all(b)?;
1124 s2.flush()
1125 };
1126 let end = snapshot_transfer_with::<EvtAdd>(
1129 &primary_journal,
1130 &mut publish,
1131 &transfer_shutdown,
1132 )
1133 .expect("snapshot transfer");
1134 assert_eq!(
1135 end,
1136 melin_transport_core::replication::catchup::CatchUpResult::Ok(5)
1137 );
1138 let mut s2_acks = s2.try_clone().expect("clone");
1139 wait_for_ack(&mut s2_acks, 5);
1140
1141 shutdown.store(true, Ordering::Relaxed);
1144 let result = replica.join().expect("replica thread panicked");
1145 assert_eq!(result, Ok(true), "receiver must exit cleanly via shutdown");
1146
1147 assert!(
1150 dir.path().join("replica.journal.divergent.0").exists(),
1151 "divergent lineage must be archived"
1152 );
1153 assert!(replica_journal.exists(), "re-seeded live journal");
1154 }
1155
1156 #[test]
1170 fn resync_transfer_failure_rebuilds_clean_not_over_stale_writer() {
1171 let dir = tempfile::tempdir().expect("tempdir");
1172
1173 let primary_journal = dir.path().join("primary.journal");
1177 let mut w = BufferedWriter::<EvtAdd>::create(&primary_journal).expect("create");
1178 for v in 1..=2u64 {
1179 w.append(&JournalEvent::App(EvtAdd(v))).expect("append");
1180 }
1181 drop(w);
1182 let (lineage_start, lineage_anchor) =
1183 lineage_origin(&primary_journal).expect("lineage");
1184
1185 let repl_key = ed25519_dalek::SigningKey::from_bytes(&[0xFC; 32]);
1186 let pub_b64 = base64::Engine::encode(
1187 &base64::engine::general_purpose::STANDARD,
1188 repl_key.verifying_key().to_bytes(),
1189 );
1190 let authorized_keys = melin_app::auth::AuthorizedKeys::parse(&format!(
1191 "replication {pub_b64} test-replica\n"
1192 ))
1193 .expect("parse keys");
1194
1195 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
1196 listener.set_nonblocking(true).expect("nonblocking");
1197 let addr = listener.local_addr().expect("addr");
1198
1199 let replica_journal = dir.path().join("replica.journal");
1200 let replica_snapshot = dir.path().join("replica.snapshot");
1201 let shutdown = Arc::new(AtomicBool::new(false));
1202 let promote = Arc::new(AtomicBool::new(false));
1203 let cores = crate::server::PipelineCores {
1204 journal: 0,
1205 matching: 0,
1206 response: 0,
1207 reader: 0,
1208 repl_sender: 0,
1209 event_publisher: 0,
1210 shadow: 0,
1211 repl_handler_0: 0,
1212 repl_handler_1: 0,
1213 };
1214 let replica = {
1215 let journal = replica_journal.clone();
1216 let shutdown = Arc::clone(&shutdown);
1217 let promote = Arc::clone(&promote);
1218 std::thread::spawn(move || -> Result<bool, String> {
1219 run_receiver::<App, BufferedWriter<EvtAdd>>(
1220 addr,
1221 &journal,
1222 &ed25519_dalek::SigningKey::from_bytes(&[0xFC; 32]),
1223 &shutdown,
1224 &promote,
1225 3_600_000,
1226 replica_snapshot,
1227 cores,
1228 Duration::ZERO,
1229 64,
1230 false,
1231 Arc::new(Factory),
1232 Arc::new(melin_transport_core::fence::FenceState::new(0)),
1233 )
1234 .map(|state| state.is_none())
1235 .map_err(|e| e.to_string())
1236 })
1237 };
1238
1239 let stream_one = |s: &mut TcpStream, buf: &mut Vec<u8>| {
1242 buf.clear();
1243 encode_stream_start(0, lineage_start, lineage_anchor, 0, buf);
1244 s.write_all(buf).expect("StreamStart");
1245 buf.clear();
1246 melin_transport_core::replication_wire::encode_input_batch(
1247 &[InputSlot::<EvtAdd> {
1248 connection_id: 0,
1249 key_hash: 0,
1250 request_seq: 0,
1251 sequence: 1,
1252 timestamp_ns: 1,
1253 event: JournalEvent::App(EvtAdd(1)),
1254 publish_ts: Default::default(),
1255 recv_ts: Default::default(),
1256 }],
1257 buf,
1258 );
1259 s.write_all(buf).expect("InputBatch");
1260 };
1261
1262 let mut buf = Vec::new();
1267 let mut s1 = accept_within(&listener, 30);
1268 let mut s1r = s1.try_clone().expect("clone");
1269 authenticate_replica(&mut s1r, &authorized_keys).expect("auth 1");
1270 match read_replica_msg(&mut s1) {
1271 ReplicaMessage::Handshake(h) => assert_eq!(h.last_sequence, 0, "fresh"),
1272 other => panic!("expected Handshake, got {other:?}"),
1273 }
1274 stream_one(&mut s1, &mut buf);
1275 wait_for_ack(&mut s1, 1);
1276 buf.clear();
1277 encode_rotate(1, &[0xEE; 32], &mut buf);
1278 s1.write_all(&buf).expect("Rotate");
1279
1280 let mut s2 = accept_within(&listener, 30);
1285 let mut s2r = s2.try_clone().expect("clone");
1286 authenticate_replica(&mut s2r, &authorized_keys).expect("auth 2");
1287 match read_replica_msg(&mut s2) {
1288 ReplicaMessage::Handshake(h) => assert_eq!(
1289 h.last_sequence, 1,
1290 "reconnect carries the recovered forked position"
1291 ),
1292 other => panic!("expected Handshake, got {other:?}"),
1293 }
1294 buf.clear();
1295 encode_hash_mismatch(&mut buf);
1296 s2.write_all(&buf).expect("HashMismatch");
1297 s2.flush().expect("flush");
1298 drop(s2);
1301 drop(s2r);
1302
1303 let mut s3 = accept_within(&listener, 30);
1308 let mut s3r = s3.try_clone().expect("clone");
1309 authenticate_replica(&mut s3r, &authorized_keys).expect("auth 3");
1310 match read_replica_msg(&mut s3) {
1311 ReplicaMessage::Handshake(h) => assert_eq!(
1312 h.last_sequence, 0,
1313 "after a failed transfer the retry handshakes as fresh"
1314 ),
1315 other => panic!("expected Handshake, got {other:?}"),
1316 }
1317 stream_one(&mut s3, &mut buf);
1318 wait_for_ack(&mut s3, 1);
1319
1320 shutdown.store(true, Ordering::Relaxed);
1321 let result = replica.join().expect("replica thread panicked");
1322 assert_eq!(result, Ok(true), "receiver exits cleanly via shutdown");
1323
1324 assert!(
1329 dir.path().join("replica.journal.divergent.0").exists(),
1330 "forked lineage archived, never deleted"
1331 );
1332 assert!(
1333 replica_journal.exists(),
1334 "fresh live journal recreated after the failed transfer"
1335 );
1336 }
1337
1338 #[test]
1345 fn second_mid_stream_divergence_exits_hard() {
1346 let dir = tempfile::tempdir().expect("tempdir");
1347
1348 let primary_journal = dir.path().join("primary.journal");
1349 let mut w = BufferedWriter::<EvtAdd>::create(&primary_journal).expect("create");
1350 let mut chain_at_4 = [0u8; 32];
1351 for v in 1..=5u64 {
1352 w.append(&JournalEvent::App(EvtAdd(v))).expect("append");
1353 if v == 2 {
1354 w.rotate_segment().expect("rotate");
1355 }
1356 if v == 4 {
1357 chain_at_4 = w.chain_hash().expect("chain");
1358 }
1359 }
1360 drop(w);
1361 melin_transport_core::snapshot::save::<App>(
1362 &App,
1363 WireSeq::new(4),
1364 chain_at_4,
1365 0,
1366 &primary_journal.with_extension("snapshot"),
1367 )
1368 .expect("save snapshot");
1369 let (lineage_start, lineage_anchor) =
1370 lineage_origin(&primary_journal).expect("lineage");
1371
1372 let repl_key = ed25519_dalek::SigningKey::from_bytes(&[0xFC; 32]);
1373 let pub_b64 = base64::Engine::encode(
1374 &base64::engine::general_purpose::STANDARD,
1375 repl_key.verifying_key().to_bytes(),
1376 );
1377 let authorized_keys = melin_app::auth::AuthorizedKeys::parse(&format!(
1378 "replication {pub_b64} test-replica\n"
1379 ))
1380 .expect("parse keys");
1381
1382 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
1383 listener.set_nonblocking(true).expect("nonblocking");
1384 let addr = listener.local_addr().expect("addr");
1385
1386 let replica_journal = dir.path().join("replica.journal");
1387 let replica_snapshot = dir.path().join("replica.snapshot");
1388 let shutdown = Arc::new(AtomicBool::new(false));
1389 let promote = Arc::new(AtomicBool::new(false));
1390 let cores = crate::server::PipelineCores {
1391 journal: 0,
1392 matching: 0,
1393 response: 0,
1394 reader: 0,
1395 repl_sender: 0,
1396 event_publisher: 0,
1397 shadow: 0,
1398 repl_handler_0: 0,
1399 repl_handler_1: 0,
1400 };
1401 let replica = {
1402 let journal = replica_journal.clone();
1403 let shutdown = Arc::clone(&shutdown);
1404 let promote = Arc::clone(&promote);
1405 std::thread::spawn(move || -> Result<bool, String> {
1406 run_receiver::<App, BufferedWriter<EvtAdd>>(
1407 addr,
1408 &journal,
1409 &ed25519_dalek::SigningKey::from_bytes(&[0xFC; 32]),
1410 &shutdown,
1411 &promote,
1412 3_600_000,
1413 replica_snapshot,
1414 cores,
1415 Duration::ZERO,
1416 64,
1417 false,
1418 Arc::new(Factory),
1419 Arc::new(melin_transport_core::fence::FenceState::new(0)),
1420 )
1421 .map(|state| state.is_none())
1422 .map_err(|e| e.to_string())
1423 })
1424 };
1425
1426 let mut s1 = accept_within(&listener, 30);
1428 let mut s1r = s1.try_clone().expect("clone");
1429 authenticate_replica(&mut s1r, &authorized_keys).expect("auth 1");
1430 match read_replica_msg(&mut s1) {
1431 ReplicaMessage::Handshake(h) => assert_eq!(h.last_sequence, 0),
1432 other => panic!("expected Handshake, got {other:?}"),
1433 }
1434 let mut buf = Vec::new();
1435 encode_stream_start(0, lineage_start, lineage_anchor, 0, &mut buf);
1436 s1.write_all(&buf).expect("StreamStart 1");
1437 buf.clear();
1438 melin_transport_core::replication_wire::encode_input_batch(
1439 &[InputSlot::<EvtAdd> {
1440 connection_id: 0,
1441 key_hash: 0,
1442 request_seq: 0,
1443 sequence: 1,
1444 timestamp_ns: 1,
1445 event: JournalEvent::App(EvtAdd(1)),
1446 publish_ts: Default::default(),
1447 recv_ts: Default::default(),
1448 }],
1449 &mut buf,
1450 );
1451 s1.write_all(&buf).expect("InputBatch");
1452 wait_for_ack(&mut s1, 1);
1453 buf.clear();
1454 encode_rotate(1, &[0xEE; 32], &mut buf);
1455 s1.write_all(&buf).expect("Rotate 1");
1456
1457 let mut s2 = accept_within(&listener, 30);
1460 let mut s2r = s2.try_clone().expect("clone");
1461 authenticate_replica(&mut s2r, &authorized_keys).expect("auth 2");
1462 match read_replica_msg(&mut s2) {
1463 ReplicaMessage::Handshake(h) => assert_eq!(h.last_sequence, 1),
1464 other => panic!("expected Handshake, got {other:?}"),
1465 }
1466 buf.clear();
1467 encode_hash_mismatch(&mut buf);
1468 s2.write_all(&buf).expect("HashMismatch");
1469 let transfer_shutdown = AtomicBool::new(false);
1470 {
1471 let mut publish = |b: &[u8]| -> std::io::Result<()> {
1472 s2.write_all(b)?;
1473 s2.flush()
1474 };
1475 snapshot_transfer_with::<EvtAdd>(
1476 &primary_journal,
1477 &mut publish,
1478 &transfer_shutdown,
1479 )
1480 .expect("snapshot transfer");
1481 }
1482 buf.clear();
1483 melin_transport_core::replication_wire::encode_input_batch(
1484 &[InputSlot::<EvtAdd> {
1485 connection_id: 0,
1486 key_hash: 0,
1487 request_seq: 0,
1488 sequence: 6,
1489 timestamp_ns: 6,
1490 event: JournalEvent::App(EvtAdd(6)),
1491 publish_ts: Default::default(),
1492 recv_ts: Default::default(),
1493 }],
1494 &mut buf,
1495 );
1496 s2.write_all(&buf).expect("InputBatch 6");
1497 let mut s2_acks = s2.try_clone().expect("clone");
1498 wait_for_ack(&mut s2_acks, 6);
1499 buf.clear();
1500 encode_rotate(6, &[0xEE; 32], &mut buf);
1501 s2.write_all(&buf).expect("Rotate 2");
1502
1503 let result = replica.join().expect("replica thread panicked");
1506 let err = result.expect_err("second divergence must exit hard");
1507 assert!(
1508 err.contains("divergence recurred"),
1509 "error must name the recurrence: {err}"
1510 );
1511 }
1512
1513 type PromoteHandles =
1522 crate::replication::ReplicaPipelineHandles<App, BufferedWriter<EvtAdd>>;
1523
1524 #[test]
1525 fn promotion_hands_back_local_state_when_present() {
1526 let dir = tempfile::tempdir().unwrap();
1527 let mut pipeline: Option<PromoteHandles> = None;
1528 let mut exchange = Some(App);
1529 let mut journal_writer =
1530 Some(BufferedWriter::<EvtAdd>::create(&dir.path().join("p.journal")).unwrap());
1531
1532 let result = crate::replication::take_pipeline_for_promotion(
1533 &mut pipeline,
1534 &mut exchange,
1535 &mut journal_writer,
1536 );
1537
1538 assert!(matches!(result, Ok(Some(_))), "warm state must be promoted");
1539 assert!(
1540 exchange.is_none() && journal_writer.is_none(),
1541 "state must be moved into the result"
1542 );
1543 }
1544
1545 #[test]
1546 fn promotion_errs_when_no_local_state() {
1547 let mut pipeline: Option<PromoteHandles> = None;
1548 let mut exchange: Option<App> = None;
1549 let mut journal_writer: Option<BufferedWriter<EvtAdd>> = None;
1550
1551 let result = crate::replication::take_pipeline_for_promotion(
1552 &mut pipeline,
1553 &mut exchange,
1554 &mut journal_writer,
1555 );
1556
1557 let err = match result {
1559 Err(e) => e,
1560 Ok(_) => panic!("a promote with nothing to promote must error"),
1561 };
1562 assert!(
1563 err.to_string().contains("no local state available"),
1564 "{err}"
1565 );
1566 }
1567
1568 #[test]
1569 fn promotion_errs_on_partial_state() {
1570 let mut pipeline: Option<PromoteHandles> = None;
1572 let mut exchange = Some(App);
1573 let mut journal_writer: Option<BufferedWriter<EvtAdd>> = None;
1574
1575 let result = crate::replication::take_pipeline_for_promotion(
1576 &mut pipeline,
1577 &mut exchange,
1578 &mut journal_writer,
1579 );
1580
1581 assert!(result.is_err(), "partial state is not a valid promotion");
1582 }
1583 }
1584}