#![cfg(feature = "dpdk")]
use std::io;
use std::marker::PhantomData;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use tracing::{debug, error, info, warn};
use melin_app::Application;
use melin_journal::BufferedWriter;
use melin_journal::replication::ReplicationConsumer;
use super::auth::{
AuthChallenge, AuthOutcome, AuthTransport, PolledAuthStream, authenticate_with_primary,
generate_challenge_nonce, step_authentication,
};
use super::join_worker::{
JoinContext, JoinRequest, JoinStallWatch, JoinStream, JoinWorker, Pump, stream_join,
};
use super::receiver_transport::{
ControlFrameSource, FrameResult, ReceiverTransport, compact_recv_buf, run_serviced,
streaming_loop, try_extract_frame,
};
use super::validation_worker::ValidationWorker;
use super::{
ReceiverResult, ReplicaCursors, ReplicaPipelineHandles, ReplicationMetrics, ResumePoint,
ResyncDecision, SentHighWater, StreamStartVerdict, build_replica_pipeline_with_threads,
handle_resync_verdict, handle_session_exit, journal_failed_while_disconnected,
recover_replica_state, sleep_then_double_backoff, take_pipeline_for_promotion,
teardown_replica_pipeline,
};
use melin_app::auth::AuthorizedKeys;
use melin_transport_core::node_store::NodeStore;
use melin_transport_core::replica_presence::{FirstAck, ReplicaPresence};
use melin_transport_core::replication::PersistWatch;
use melin_transport_core::replication::catchup::CatchUpPublisher;
use melin_transport_core::replication::handoff::{
HandoffIo, HandoffStep, LiveHandoff, PassProgress,
};
use melin_transport_core::replication::protocol::{
Ack, Handshake, MAX_CONTROL_FRAME, PrimaryMessage, ReplicaMessage, decode_primary_message,
decode_replica_message, encode_ack, encode_challenge, encode_handshake, encode_heartbeat,
};
use melin_transport_core::replication::validate::{
HandshakeValidation, validate_replica_handshake_settled,
};
struct DpdkReceiverTransport<'a> {
transport: &'a mut melin_dpdk::DpdkTransport,
handle: melin_dpdk::SocketHandle,
send_buf: Vec<u8>,
}
const ACK_RETRY_CAP: u32 = 32;
pub(crate) const REPL_DISPATCH_BURST: usize = 256;
const MAX_REPL_BYTES_PER_TICK: usize = 128 * 1024;
impl ReceiverTransport for DpdkReceiverTransport<'_> {
fn poll_recv(&mut self, recv_buf: &mut Vec<u8>) -> io::Result<bool> {
self.transport.poll();
let before = recv_buf.len();
self.transport.recv_into_vec(self.handle, recv_buf);
Ok(recv_buf.len() > before)
}
fn send_ack(&mut self, ack: &Ack) -> io::Result<bool> {
self.send_buf.clear();
encode_ack(ack, &mut self.send_buf);
let mut attempts: u32 = 0;
loop {
if self.transport.queue_send(self.handle, &self.send_buf) {
self.transport.poll();
return Ok(true);
}
attempts += 1;
if attempts >= ACK_RETRY_CAP {
tracing::warn!("DPDK ack send failed after {ACK_RETRY_CAP} retries");
return Ok(false);
}
self.transport.poll();
if !self.transport.is_connected(self.handle) {
return Err(io::Error::other("replica disconnected during ack send"));
}
}
}
fn ack_in_flight(&self) -> bool {
false
}
fn is_connected(&mut self) -> bool {
self.transport.is_connected(self.handle)
}
fn keep_link_serviced(&mut self) {
self.transport.poll();
}
}
const REPLICATION_LIVENESS: melin_dpdk::PeerLiveness = melin_dpdk::PeerLiveness::REPLICATION;
const JOIN_STALL_LIMIT: std::time::Duration = REPLICATION_LIVENESS.timeout();
const SESSION_END_LINGER: std::time::Duration = std::time::Duration::from_millis(100);
fn linger_then_reset(
transport: &mut melin_dpdk::DpdkTransport,
handles: &[melin_dpdk::SocketHandle],
) {
let deadline = std::time::Instant::now() + SESSION_END_LINGER;
while std::time::Instant::now() < deadline
&& handles
.iter()
.any(|&h| transport.is_active(h) && !transport.tx_drained(h))
{
transport.poll();
std::thread::yield_now();
}
for &handle in handles {
transport.reset(handle);
}
}
#[derive(Debug, PartialEq, Eq)]
enum JoinFrames {
Malformed,
Read { acked: bool },
}
fn keep_last_ack(buf: &mut Vec<u8>) -> JoinFrames {
let mut consumed = 0;
let mut newest: Option<(usize, usize)> = None;
loop {
match try_extract_frame(&buf[consumed..], MAX_CONTROL_FRAME) {
FrameResult::Complete(payload_start, frame_end) => {
let payload = &buf[consumed + payload_start..consumed + frame_end];
if matches!(decode_replica_message(payload), Ok(ReplicaMessage::Ack(_))) {
newest = Some((consumed, consumed + frame_end));
}
consumed += frame_end;
}
FrameResult::Incomplete => break,
FrameResult::Oversized => return JoinFrames::Malformed,
}
}
let acked = newest.is_some();
match newest {
Some((start, end)) => {
let len = buf.len();
let ack_len = end - start;
buf.copy_within(start..end, 0);
buf.copy_within(consumed..len, ack_len);
buf.truncate(ack_len + (len - consumed));
}
None => compact_recv_buf(buf, consumed),
}
JoinFrames::Read { acked }
}
fn end_session(transport: &mut melin_dpdk::DpdkTransport, handle: melin_dpdk::SocketHandle) {
linger_then_reset(transport, &[handle]);
}
enum SlotState {
Idle,
Authenticating(melin_dpdk::SocketHandle),
Handshaking(melin_dpdk::SocketHandle),
Joining(melin_dpdk::SocketHandle),
Streaming(melin_dpdk::SocketHandle, FirstAck),
}
const AUTH_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
struct PendingValidation {
handshake: Handshake,
verdict_rx: std::sync::mpsc::Receiver<io::Result<HandshakeValidation>>,
}
struct PendingJoin {
phase: JoinPhase,
stream_base: u64,
stall: JoinStallWatch,
first_ack: FirstAck,
}
enum JoinPhase {
CatchUp(JoinStream),
Handoff {
handoff: LiveHandoff,
pass: Option<JoinStream>,
},
}
struct HandoffSocket<'a> {
transport: &'a mut melin_dpdk::DpdkTransport,
handle: melin_dpdk::SocketHandle,
joiner: &'a JoinWorker,
pass: &'a mut Option<JoinStream>,
queued: usize,
blocked: bool,
worker_gone: bool,
}
impl HandoffIo for HandoffSocket<'_> {
fn start_pass(&mut self, from: u64) -> io::Result<()> {
match self.joiner.submit(JoinRequest::HandoffPass { from }) {
Ok(stream) => {
*self.pass = Some(stream);
Ok(())
}
Err(_) => {
self.worker_gone = true;
Err(io::Error::other("the replica's join worker is gone"))
}
}
}
fn pump_pass(&mut self) -> PassProgress {
let Some(stream) = self.pass.as_mut() else {
return PassProgress::Done(Err(io::Error::other("no handoff pass is running")));
};
if self.queued >= MAX_REPL_BYTES_PER_TICK {
self.blocked = false;
return PassProgress::Pending;
}
let budget = MAX_REPL_BYTES_PER_TICK - self.queued;
let capacity = self.transport.max_tx_queue_size(self.handle);
let (transport, handle) = (&mut *self.transport, self.handle);
match stream.pump(budget, capacity, |frame| {
transport.queue_send(handle, frame)
}) {
Pump::Pending { queued } => {
self.queued += queued;
self.blocked = stream.is_blocked();
PassProgress::Pending
}
Pump::Done(result) => {
*self.pass = None;
self.blocked = false;
PassProgress::Done(result)
}
Pump::WorkerGone => {
*self.pass = None;
self.worker_gone = true;
PassProgress::Done(Err(io::Error::other("the replica's join worker died")))
}
}
}
fn try_send(&mut self, frame: &[u8]) -> bool {
if self.queued >= MAX_REPL_BYTES_PER_TICK {
self.blocked = false;
return false;
}
let taken = self.transport.queue_send(self.handle, frame);
if taken {
self.queued += frame.len();
}
self.blocked = !taken;
taken
}
}
enum JoinTick {
Pending { queued: usize, blocked: bool },
CaughtUp(u64),
Live(SentHighWater),
Failed,
}
struct DpdkReplicaSlot {
state: SlotState,
consumer: ReplicationConsumer,
active_flag: Arc<AtomicBool>,
evict_flag: Arc<AtomicBool>,
recv_buf: Vec<u8>,
send_buf: Vec<u8>,
last_send: std::time::Instant,
persist_watch: PersistWatch,
sent: SentHighWater,
pending_validation: Option<PendingValidation>,
validator: ValidationWorker,
join: Option<PendingJoin>,
joiner: JoinWorker,
auth: Option<AuthChallenge>,
}
impl AuthTransport for melin_dpdk::DpdkTransport {
type Handle = melin_dpdk::SocketHandle;
fn is_active(&mut self, handle: Self::Handle) -> bool {
self.is_connected(handle)
}
fn recv_into_vec(&mut self, handle: Self::Handle, dest: &mut Vec<u8>) {
self.recv_into_vec(handle, dest);
}
fn queue_send(&mut self, handle: Self::Handle, data: &[u8]) -> bool {
self.queue_send(handle, data)
}
fn poll(&mut self) {
self.poll();
}
}
impl DpdkReplicaSlot {
fn go_idle(
&mut self,
slot_idx: usize,
transport: &mut melin_dpdk::DpdkTransport,
cursors: &ReplicaCursors,
metrics: &ReplicationMetrics,
replicas_connected: &ReplicaPresence,
) {
if let SlotState::Authenticating(h)
| SlotState::Handshaking(h)
| SlotState::Joining(h)
| SlotState::Streaming(h, _) = self.state
{
let was_counted =
matches!(self.state, SlotState::Joining(_) | SlotState::Streaming(..));
transport.reset(h);
cursors.clear_on_disconnect(slot_idx);
self.active_flag.store(false, Ordering::Release);
metrics.catching_up[slot_idx].store(false, Ordering::Relaxed);
if was_counted {
replicas_connected.lower();
}
}
self.state = SlotState::Idle;
self.recv_buf.clear();
self.auth = None;
self.pending_validation = None;
self.join = None;
}
}
pub struct DpdkReplicationDriver<A: Application> {
slots: [DpdkReplicaSlot; 2],
cursors: ReplicaCursors,
replica_ready: Arc<AtomicBool>,
replicas_connected: Arc<ReplicaPresence>,
metrics: Arc<ReplicationMetrics>,
batch_size: usize,
heartbeat_interval: std::time::Duration,
fence_state: Arc<melin_transport_core::fence::FenceState>,
authorized_keys: Arc<AuthorizedKeys>,
_app: PhantomData<fn(A)>,
}
impl<A: Application> DpdkReplicationDriver<A> {
#[allow(clippy::too_many_arguments)]
pub fn new(
repl_consumers: [ReplicationConsumer; 2],
replica_slots: Arc<melin_transport_core::ReplicaSlotCursors>,
store: NodeStore,
replica_ready: Arc<AtomicBool>,
replicas_connected: Arc<ReplicaPresence>,
evict_flags: [Arc<AtomicBool>; 2],
active_flags: [Arc<AtomicBool>; 2],
metrics: Arc<ReplicationMetrics>,
batch_size: usize,
heartbeat_secs: u64,
fence_state: Arc<melin_transport_core::fence::FenceState>,
authorized_keys: Arc<AuthorizedKeys>,
) -> Result<Self, String> {
let [consumer_0, consumer_1] = repl_consumers;
let now = std::time::Instant::now();
let [validator_0, validator_1] = [0usize, 1].map(|idx| {
ValidationWorker::spawn(
format!("repl-validate-{idx}"),
store.journal().to_path_buf(),
validate_replica_handshake_settled,
)
});
let validator_0 =
validator_0.map_err(|e| format!("spawn handshake validation worker 0: {e}"))?;
let validator_1 =
validator_1.map_err(|e| format!("spawn handshake validation worker 1: {e}"))?;
let [joiner_0, joiner_1] = [0usize, 1].map(|idx| {
let ctx = JoinContext {
store: store.clone(),
fence_state: Arc::clone(&fence_state),
};
JoinWorker::spawn(
format!("repl-join-{idx}"),
move |request: &JoinRequest, publish: CatchUpPublisher<'_>, cancel: &AtomicBool| {
stream_join::<A::Event>(&ctx, request, publish, cancel)
},
)
});
let joiner_0 = joiner_0.map_err(|e| format!("spawn replica join worker 0: {e}"))?;
let joiner_1 = joiner_1.map_err(|e| format!("spawn replica join worker 1: {e}"))?;
Ok(DpdkReplicationDriver {
slots: [
DpdkReplicaSlot {
state: SlotState::Idle,
consumer: consumer_0,
active_flag: Arc::clone(&active_flags[0]),
evict_flag: Arc::clone(&evict_flags[0]),
recv_buf: Vec::with_capacity(4096),
send_buf: Vec::with_capacity(512 * 1024),
last_send: now,
persist_watch: PersistWatch::default(),
sent: SentHighWater::seed(0, 0),
pending_validation: None,
validator: validator_0,
join: None,
joiner: joiner_0,
auth: None,
},
DpdkReplicaSlot {
state: SlotState::Idle,
consumer: consumer_1,
active_flag: Arc::clone(&active_flags[1]),
evict_flag: Arc::clone(&evict_flags[1]),
recv_buf: Vec::with_capacity(4096),
send_buf: Vec::with_capacity(512 * 1024),
last_send: now,
persist_watch: PersistWatch::default(),
sent: SentHighWater::seed(0, 0),
pending_validation: None,
validator: validator_1,
join: None,
joiner: joiner_1,
auth: None,
},
],
cursors: ReplicaCursors::new(replica_slots, metrics.clone()),
replica_ready,
replicas_connected,
metrics,
batch_size,
heartbeat_interval: std::time::Duration::from_secs(heartbeat_secs),
fence_state,
authorized_keys,
_app: PhantomData,
})
}
pub fn accept_connection(
&mut self,
peer: std::net::SocketAddr,
handle: melin_dpdk::SocketHandle,
transport: &mut melin_dpdk::DpdkTransport,
) {
let idle_slot = self
.slots
.iter()
.position(|s| matches!(s.state, SlotState::Idle));
let Some(idx) = idle_slot else {
debug!(peer = ?peer, "replica rejected — both slots occupied");
transport.reset(handle);
return;
};
transport.set_peer_liveness(handle, &REPLICATION_LIVENESS);
let nonce = match generate_challenge_nonce() {
Ok(n) => n,
Err(e) => {
warn!(peer = ?peer, error = %e, "failed to generate auth challenge — dropping replica");
transport.reset(handle);
return;
}
};
let slot = &mut self.slots[idx];
slot.recv_buf.clear();
slot.send_buf.clear();
encode_challenge(&nonce, &mut slot.send_buf);
if !transport.queue_send(handle, &slot.send_buf) {
warn!(peer = ?peer, slot = idx, "TX queue full sending auth challenge — dropping replica");
transport.reset(handle);
return;
}
info!(peer = ?peer, slot = idx, "replica connected via DPDK — authenticating");
slot.auth = Some(AuthChallenge {
nonce,
deadline: std::time::Instant::now() + AUTH_TIMEOUT,
});
slot.state = SlotState::Authenticating(handle);
}
pub fn tick(
&mut self,
transport: &mut melin_dpdk::DpdkTransport,
shutdown: &AtomicBool,
) -> bool {
let slots = &mut self.slots;
let cursors = &self.cursors;
let replica_ready = &self.replica_ready;
let replicas_connected = &self.replicas_connected;
let metrics = &self.metrics;
let fence_state = &self.fence_state;
let authorized_keys = &self.authorized_keys;
let batch_size = self.batch_size;
let heartbeat_interval = self.heartbeat_interval;
for (i, slot) in slots.iter_mut().enumerate() {
let evicting =
slot.evict_flag.load(Ordering::Acquire) && !matches!(slot.state, SlotState::Idle);
if !evicting {
continue;
}
metrics.evictions_total.fetch_add(1, Ordering::Relaxed);
warn!(
slot = i,
"evicting slow replica (ring backpressure timeout, DPDK)"
);
slot.consumer.skip_to_producer();
slot.evict_flag.store(false, Ordering::Release);
slot.go_idle(i, transport, cursors, metrics, replicas_connected);
}
let mut any_active = false;
for (slot_idx, slot) in slots.iter_mut().enumerate() {
match slot.state {
SlotState::Idle => {
while let Some(batch) = slot.consumer.peek() {
batch.commit();
}
}
SlotState::Authenticating(handle) => {
any_active = true;
let challenge = slot
.auth
.as_ref()
.expect("auth challenge present while Authenticating");
match step_authentication(
transport,
handle,
challenge,
&mut slot.recv_buf,
&mut slot.send_buf,
authorized_keys,
slot_idx,
) {
AuthOutcome::Pending => {}
AuthOutcome::Authenticated => {
slot.auth = None;
slot.state = SlotState::Handshaking(handle);
}
AuthOutcome::Rejected => {
slot.go_idle(slot_idx, transport, cursors, metrics, replicas_connected)
}
}
continue;
}
SlotState::Handshaking(handle) => {
any_active = true;
if !transport.is_connected(handle) {
warn!(
slot = slot_idx,
"replica disconnected during handshake (DPDK)"
);
slot.go_idle(slot_idx, transport, cursors, metrics, replicas_connected);
continue;
}
if let Some(pv) = slot.pending_validation.take() {
let res = match pv.verdict_rx.try_recv() {
Err(std::sync::mpsc::TryRecvError::Empty) => {
slot.pending_validation = Some(pv);
continue;
}
Err(std::sync::mpsc::TryRecvError::Disconnected) => {
error!(
slot = slot_idx,
"handshake validation worker died — disconnecting"
);
slot.go_idle(
slot_idx,
transport,
cursors,
metrics,
replicas_connected,
);
continue;
}
Ok(res) => res,
};
let h = pv.handshake;
let divergent = match res {
Ok(HandshakeValidation::Ok) => false,
Ok(HandshakeValidation::Divergent(kind)) => {
metrics.divergence_total.fetch_add(1, Ordering::Relaxed);
warn!(
slot = slot_idx,
last_sequence = h.last_sequence,
?kind,
"replica journal divergent at handshake — \
routing through snapshot resync"
);
true
}
Err(e) => {
warn!(slot = slot_idx, error = %e, "handshake validation failed — disconnecting");
slot.go_idle(
slot_idx,
transport,
cursors,
metrics,
replicas_connected,
);
continue;
}
};
let stream_base = if divergent { 0 } else { h.last_sequence };
match slot.joiner.submit(JoinRequest::Replica {
last_sequence: h.last_sequence,
divergent,
}) {
Ok(stream) => {
slot.join = Some(PendingJoin {
phase: JoinPhase::CatchUp(stream),
stream_base,
stall: JoinStallWatch::default(),
first_ack: replicas_connected.lift(),
});
slot.state = SlotState::Joining(handle);
}
Err(e) => {
error!(slot = slot_idx, error = %e, "cannot start the replica's join — disconnecting");
slot.go_idle(
slot_idx,
transport,
cursors,
metrics,
replicas_connected,
);
}
}
continue;
}
transport.recv_into_vec(handle, &mut slot.recv_buf);
match try_extract_frame(&slot.recv_buf, MAX_CONTROL_FRAME) {
FrameResult::Complete(payload_start, frame_end) => {
let payload = &slot.recv_buf[payload_start..frame_end];
match decode_replica_message(payload) {
Ok(ReplicaMessage::Handshake(h)) => {
info!(
slot = slot_idx,
last_sequence = h.last_sequence,
replica_epoch = h.epoch,
"replica handshake received (DPDK)"
);
let our_epoch = fence_state.epoch();
if let Some(first_latch) =
fence_state.fence_if_superseded(h.epoch, shutdown)
{
if first_latch {
error!(
slot = slot_idx,
replica_epoch = h.epoch,
our_epoch,
"fenced: a replica advertises a higher epoch — \
this primary has been superseded; self-demoting \
and shutting down (DPDK)"
);
}
slot.go_idle(
slot_idx,
transport,
cursors,
metrics,
replicas_connected,
);
continue;
}
metrics.catching_up[slot_idx].store(true, Ordering::Relaxed);
compact_recv_buf(&mut slot.recv_buf, frame_end);
match slot.validator.submit(h.clone()) {
Ok(verdict_rx) => {
slot.pending_validation = Some(PendingValidation {
handshake: h,
verdict_rx,
});
}
Err(e) => {
error!(slot = slot_idx, error = %e, "cannot validate handshake — disconnecting");
slot.go_idle(
slot_idx,
transport,
cursors,
metrics,
replicas_connected,
);
}
}
}
Ok(ReplicaMessage::Ack(_)) => {
warn!(
slot = slot_idx,
"expected Handshake, got Ack — disconnecting"
);
slot.go_idle(
slot_idx,
transport,
cursors,
metrics,
replicas_connected,
);
}
Err(e) => {
warn!(slot = slot_idx, error = %e, "failed to decode handshake — disconnecting");
slot.go_idle(
slot_idx,
transport,
cursors,
metrics,
replicas_connected,
);
}
}
}
FrameResult::Oversized => {
warn!(slot = slot_idx, "oversized handshake frame — disconnecting");
slot.go_idle(slot_idx, transport, cursors, metrics, replicas_connected);
}
FrameResult::Incomplete => {} }
}
SlotState::Joining(handle) => {
any_active = true;
if !transport.is_connected(handle) {
warn!(
slot = slot_idx,
"replica disconnected during catch-up (DPDK)"
);
slot.go_idle(slot_idx, transport, cursors, metrics, replicas_connected);
continue;
}
let join = slot.join.as_mut().expect("join in progress while Joining");
transport.recv_into_vec(handle, &mut slot.recv_buf);
match keep_last_ack(&mut slot.recv_buf) {
JoinFrames::Malformed => {
warn!(
slot = slot_idx,
"malformed (zero-length or oversized) frame from replica during catch-up — disconnecting"
);
slot.go_idle(slot_idx, transport, cursors, metrics, replicas_connected);
continue;
}
JoinFrames::Read { acked: true } => join.stall.arm(),
JoinFrames::Read { acked: false } => {}
}
let now = std::time::Instant::now();
let tick = match &mut join.phase {
JoinPhase::CatchUp(stream) => {
let capacity = transport.max_tx_queue_size(handle);
match stream.pump(MAX_REPL_BYTES_PER_TICK, capacity, |frame| {
transport.queue_send(handle, frame)
}) {
Pump::Pending { queued } => JoinTick::Pending {
queued,
blocked: stream.is_blocked(),
},
Pump::Done(Ok(end)) => JoinTick::CaughtUp(end),
Pump::Done(Err(e)) => {
warn!(slot = slot_idx, error = %e, "catch-up/snapshot failed — disconnecting");
JoinTick::Failed
}
Pump::WorkerGone => {
error!(
slot = slot_idx,
"replica join worker died — disconnecting"
);
JoinTick::Failed
}
}
}
JoinPhase::Handoff { handoff, pass } => {
let mut io = HandoffSocket {
transport: &mut *transport,
handle,
joiner: &slot.joiner,
pass,
queued: 0,
blocked: false,
worker_gone: false,
};
match handoff.step(&mut slot.consumer, &slot.active_flag, &mut io, now)
{
Ok(HandoffStep::Pending) => JoinTick::Pending {
queued: io.queued,
blocked: io.blocked,
},
Ok(HandoffStep::Live(sent)) => JoinTick::Live(sent),
Err(e) if io.worker_gone => {
error!(slot = slot_idx, error = %e, "replica join worker died during the handoff — disconnecting");
JoinTick::Failed
}
Err(e) => {
warn!(slot = slot_idx, error = %e, "catch-up handoff failed — disconnecting");
JoinTick::Failed
}
}
}
};
match tick {
JoinTick::Pending { queued, blocked } => {
if queued > 0 {
transport.poll();
}
let stalled = join.stall.observe(queued > 0, blocked, now);
if stalled >= JOIN_STALL_LIMIT {
warn!(
slot = slot_idx,
stalled_ms = stalled.as_millis() as u64,
"replica stopped reading its catch-up or handoff — disconnecting"
);
slot.go_idle(
slot_idx,
transport,
cursors,
metrics,
replicas_connected,
);
}
}
JoinTick::CaughtUp(catchup_end) => {
transport.poll();
cursors.seed_on_handshake(slot_idx, join.stream_base);
join.phase = JoinPhase::Handoff {
handoff: LiveHandoff::new(join.stream_base, catchup_end),
pass: None,
};
join.stall.restart();
}
JoinTick::Live(sent) => {
transport.poll();
super::log_replica_live(slot_idx, &sent);
slot.sent = sent;
let first_ack = slot
.join
.take()
.expect("join in progress while Joining")
.first_ack;
slot.last_send = now;
slot.persist_watch = PersistWatch::default();
replica_ready.store(true, Ordering::Release);
metrics.catching_up[slot_idx].store(false, Ordering::Relaxed);
slot.state = SlotState::Streaming(handle, first_ack);
}
JoinTick::Failed => {
slot.go_idle(slot_idx, transport, cursors, metrics, replicas_connected);
}
}
}
SlotState::Streaming(handle, _) => {
any_active = true;
transport.recv_into_vec(handle, &mut slot.recv_buf);
let mut consumed = 0;
let mut ack_error = false;
let mut acked = false;
let mut ack_received: Option<std::time::Instant> = None;
loop {
let remaining = &slot.recv_buf[consumed..];
match try_extract_frame(remaining, MAX_CONTROL_FRAME) {
FrameResult::Complete(payload_start, frame_end) => {
let payload = &remaining[payload_start..frame_end];
if let Ok(ReplicaMessage::Ack(ack)) =
decode_replica_message(payload)
{
if cursors.record_ack(slot_idx, &ack, slot.sent.get()).is_err()
{
ack_error = true;
break;
}
acked = true;
let received =
*ack_received.get_or_insert_with(std::time::Instant::now);
if let Some(event) = slot.persist_watch.observe(&ack, received)
{
event.log(slot_idx);
}
}
consumed += frame_end;
}
FrameResult::Oversized => {
warn!(
slot = slot_idx,
"oversized ack frame from replica — disconnecting"
);
ack_error = true;
break;
}
FrameResult::Incomplete => break,
}
}
compact_recv_buf(&mut slot.recv_buf, consumed);
if acked {
metrics.ack_latency_us[slot_idx].store(
slot.last_send.elapsed().as_micros() as u64,
Ordering::Relaxed,
);
if let SlotState::Streaming(_, first_ack) = &mut slot.state {
first_ack.acked(replicas_connected);
}
}
if ack_error {
slot.go_idle(slot_idx, transport, cursors, metrics, replicas_connected);
continue;
}
let max_tx = transport.max_tx_queue_size(handle);
let used = transport.tx_queue_bytes(handle);
let mut available = max_tx.saturating_sub(used);
slot.send_buf.clear();
let mut batches_sent = 0;
let mut bytes_this_tick = 0usize;
let mut tx_overflow = false;
while batches_sent < batch_size {
if batches_sent > 0 && bytes_this_tick >= MAX_REPL_BYTES_PER_TICK {
break;
}
let Some(batch) = slot.consumer.peek() else {
break;
};
let data_len = batch.data().len();
if data_len > available {
if !slot.send_buf.is_empty() {
metrics.bytes_sent[slot_idx]
.fetch_add(slot.send_buf.len() as u64, Ordering::Relaxed);
if !transport.queue_send(handle, &slot.send_buf) {
slot.send_buf.clear();
tx_overflow = true;
break;
}
slot.send_buf.clear();
}
transport.poll();
let used = transport.tx_queue_bytes(handle);
available = max_tx.saturating_sub(used);
if data_len > available {
break;
}
}
slot.send_buf.extend_from_slice(batch.data());
let end_sequence = batch.meta().end_sequence;
batch.commit();
slot.sent.advance(end_sequence);
batches_sent += 1;
bytes_this_tick += data_len;
available = available.saturating_sub(data_len);
}
if !tx_overflow && !slot.send_buf.is_empty() {
metrics.bytes_sent[slot_idx]
.fetch_add(slot.send_buf.len() as u64, Ordering::Relaxed);
if !transport.queue_send(handle, &slot.send_buf) {
tx_overflow = true;
}
}
if tx_overflow {
slot.send_buf.clear();
warn!(
slot = slot_idx,
"TX overflow on replica socket — disconnecting"
);
slot.go_idle(slot_idx, transport, cursors, metrics, replicas_connected);
continue;
}
if !slot.send_buf.is_empty() {
transport.poll();
slot.last_send = std::time::Instant::now();
}
if batches_sent == 0 && slot.last_send.elapsed() >= heartbeat_interval {
slot.send_buf.clear();
encode_heartbeat(slot.sent.get(), &mut slot.send_buf);
transport.queue_send(handle, &slot.send_buf);
slot.last_send = std::time::Instant::now();
}
if !transport.is_connected(handle) {
warn!(slot = slot_idx, "replica disconnected (DPDK)");
slot.go_idle(slot_idx, transport, cursors, metrics, replicas_connected);
continue;
}
}
}
}
any_active
}
pub fn close_all_links(self, transport: &mut melin_dpdk::DpdkTransport) {
let handles: Vec<_> = self
.slots
.iter()
.filter_map(|slot| match slot.state {
SlotState::Authenticating(h)
| SlotState::Handshaking(h)
| SlotState::Joining(h)
| SlotState::Streaming(h, _) => Some(h),
_ => None,
})
.collect();
linger_then_reset(transport, &handles);
}
}
#[allow(clippy::too_many_arguments)]
pub fn run_receiver_dpdk<A>(
transport: &mut melin_dpdk::DpdkTransport,
primary_ip: std::net::Ipv4Addr,
primary_port: u16,
signing_key: &ed25519_dalek::SigningKey,
store: &NodeStore,
shutdown: &AtomicBool,
control: &super::ReplicaControlPlane,
snapshot_interval_ms: u64,
cores: crate::layout::PipelineCores,
staging_mode: melin_journal::StagingMode,
group_commit_delay: std::time::Duration,
pipeline_depth: usize,
fence_state: Arc<melin_transport_core::fence::FenceState>,
sizing: &A::Sizing,
) -> ReceiverResult<A, BufferedWriter<A::Event>>
where
A: Application + Send + 'static,
A::Event: Send + Sync + 'static,
A::Report: Send + 'static,
A::QueryResponse: Send + 'static,
{
let (mut app, mut journal_writer, mut last_sequence, mut chain_hash) =
recover_replica_state::<A, BufferedWriter<A::Event>>(
store,
&fence_state,
&control.lineage_ack_policy,
sizing,
)?;
let super::ReplicaControlPlane {
promote,
tip_ready,
journal_tip,
primary_link_up,
primary_observed: _,
lineage_ack_policy,
pipeline_healthy,
} = control;
journal_tip.advance(melin_transport_core::WireSeq::new(last_sequence));
tip_ready.store(true, Ordering::Release);
let mut backoff = std::time::Duration::from_secs(1);
let mut divergence_resyncs: u32 = 0;
let mut send_buf = Vec::with_capacity(64);
let mut recv_buf: Vec<u8> = Vec::with_capacity(4096);
let mut local_port: u16 = 40000;
let mut pipeline: Option<ReplicaPipelineHandles<A, BufferedWriter<A::Event>>> = None;
loop {
primary_link_up.store(false, Ordering::Release);
if let Some(p) = pipeline.as_ref() {
match p
.settled_resume_point(|| shutdown.load(Ordering::Relaxed) || promote.is_requested())
{
ResumePoint::Settled(fsync_state) => {
last_sequence = fsync_state.journal_seq.get();
chain_hash = fsync_state.chain_hash;
}
ResumePoint::Interrupted => {}
ResumePoint::JournalFailed => {
let after = handle_session_exit(
journal_failed_while_disconnected(),
&mut pipeline,
&mut divergence_resyncs,
&mut backoff,
last_sequence,
store,
&fence_state,
lineage_ack_policy,
shutdown,
promote,
|| {},
sizing,
);
if let Some(r) = after.adopt(
&mut app,
&mut journal_writer,
&mut last_sequence,
&mut chain_hash,
) {
return r;
}
continue;
}
}
}
if shutdown.load(Ordering::Relaxed) {
if let Some(p) = pipeline.take() {
let _ = teardown_replica_pipeline::<A, BufferedWriter<A::Event>>(p);
}
return Ok(None);
}
if promote.is_requested() {
info!("promotion triggered while disconnected");
return take_pipeline_for_promotion(&mut pipeline, &mut app, &mut journal_writer);
}
info!(
primary_ip = %primary_ip,
primary_port,
"connecting to primary as replica (DPDK)"
);
let mac_source = transport.seed_peer_neighbor(primary_ip);
info!(
primary_ip = %primary_ip,
mac_source = mac_source.as_str(),
"seeded primary MAC into the neighbour cache"
);
transport.poll();
const REPL_RX_BUF: usize = 512 * 1024;
const REPL_TX_BUF: usize = 64 * 1024;
const REPL_TX_QUEUE: usize = 64 * 1024;
let dial_port = local_port;
local_port = local_port.wrapping_add(1).max(40000);
let handle = match transport.connect_to_with_buffers(
primary_ip,
primary_port,
dial_port,
REPL_RX_BUF,
REPL_TX_BUF,
REPL_TX_QUEUE,
REPL_DISPATCH_BURST,
) {
Ok(handle) => handle,
Err(e) => {
warn!(
primary_ip = %primary_ip,
primary_port,
local_port = dial_port,
error = ?e,
backoff_secs = backoff.as_secs(),
"the stack refused to open a connection to the primary (DPDK) — retrying"
);
sleep_then_double_backoff(&mut backoff, shutdown, promote);
continue;
}
};
let connect_start = std::time::Instant::now();
const CONNECT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
let connected = loop {
if shutdown.load(Ordering::Relaxed) || promote.is_requested() {
break false;
}
transport.poll();
if transport.is_connected(handle) {
break true;
}
if !transport.is_active(handle) || connect_start.elapsed() >= CONNECT_TIMEOUT {
break false;
}
std::thread::yield_now();
};
if !connected {
if shutdown.load(Ordering::Relaxed) {
debug!("connect aborted — shutting down (DPDK)");
} else if promote.is_requested() {
debug!("connect aborted — promoting (DPDK)");
} else {
warn!(
backoff_secs = backoff.as_secs(),
"failed to connect to primary (DPDK) — retrying"
);
}
transport.reset(handle);
sleep_then_double_backoff(&mut backoff, shutdown, promote);
continue;
}
info!("connected to primary (DPDK)");
transport.set_peer_liveness(handle, &REPLICATION_LIVENESS);
recv_buf.clear();
let auth_result = {
let mut auth_stream = PolledAuthStream {
transport: &mut *transport,
handle,
recv_buf: &mut recv_buf,
shutdown,
deadline: std::time::Instant::now() + AUTH_TIMEOUT,
};
authenticate_with_primary(&mut auth_stream, signing_key)
};
if let Err(e) = auth_result {
transport.reset(handle);
if shutdown.load(Ordering::Relaxed) {
debug!("authentication aborted — shutting down (DPDK)");
} else {
warn!(
error = %e,
backoff_secs = backoff.as_secs(),
"authentication failed (DPDK) — retrying"
);
}
sleep_then_double_backoff(&mut backoff, shutdown, promote);
continue;
}
info!("authenticated with primary (DPDK)");
primary_link_up.store(true, Ordering::Release);
send_buf.clear();
let handshake = Handshake {
last_sequence,
chain_hash,
epoch: fence_state.epoch(),
};
encode_handshake(&handshake, &mut send_buf);
transport.queue_send(handle, &send_buf);
send_buf.clear();
macro_rules! fatal_err_dpdk {
($msg:expr) => {{
transport.reset(handle);
if let Some(p) = pipeline.take() {
let _ = teardown_replica_pipeline::<A, BufferedWriter<A::Event>>(p);
}
return Err($msg);
}};
}
recv_buf.clear();
let stream_lineage: Option<(u64, [u8; 32], Option<u64>)> = 'handshake: loop {
if shutdown.load(Ordering::Relaxed) {
transport.reset(handle);
break 'handshake None;
}
transport.poll();
transport.recv_into_vec(handle, &mut recv_buf);
match try_extract_frame(&recv_buf, MAX_CONTROL_FRAME) {
FrameResult::Complete(payload_start, frame_end) => {
let payload = &recv_buf[payload_start..frame_end];
let response = match decode_primary_message(payload) {
Ok(response) => response,
Err(e) => fatal_err_dpdk!(e.into()),
};
compact_recv_buf(&mut recv_buf, frame_end);
match response {
PrimaryMessage::StreamStart {
start_sequence,
segment_start_sequence,
anchor_hash,
genesis_entries,
epoch,
} => {
match super::accept_stream_start(
super::StreamStart {
start_sequence,
lineage: (segment_start_sequence, anchor_hash, genesis_entries),
epoch,
},
"dpdk",
&pipeline,
&mut journal_writer,
&fence_state,
control,
) {
Ok(StreamStartVerdict::Follow(lineage)) => {
break 'handshake Some(lineage);
}
Ok(StreamStartVerdict::StalePrimary) => {
transport.reset(handle);
sleep_then_double_backoff(&mut backoff, shutdown, promote);
break 'handshake None; }
Err(e) => fatal_err_dpdk!(e),
}
}
ref resync @ (PrimaryMessage::NeedSnapshot
| PrimaryMessage::HashMismatch) => {
let divergent = matches!(resync, PrimaryMessage::HashMismatch);
let decision = handle_resync_verdict(
divergent,
&mut DpdkFrameSource {
transport: &mut *transport,
handle,
recv_buf: &mut recv_buf,
shutdown,
},
&mut pipeline,
&mut app,
&mut journal_writer,
store,
&fence_state,
control,
&mut last_sequence,
&mut chain_hash,
);
match decision {
Ok(ResyncDecision::Ready {
segment_start_sequence,
anchor_hash,
genesis_entries,
resume_sequence,
}) => {
last_sequence = resume_sequence;
break 'handshake Some((
segment_start_sequence,
anchor_hash,
genesis_entries,
));
}
Ok(ResyncDecision::Retry) => {
transport.reset(handle);
sleep_then_double_backoff(&mut backoff, shutdown, promote);
break 'handshake None; }
Err(e) => fatal_err_dpdk!(e),
}
}
other => {
fatal_err_dpdk!(format!("unexpected response: {other:?}").into());
}
}
}
FrameResult::Oversized => {
fatal_err_dpdk!("oversized frame from primary during handshake".into());
}
FrameResult::Incomplete => {}
}
if !transport.is_connected(handle) {
warn!("disconnected from primary during handshake (DPDK)");
transport.reset(handle);
sleep_then_double_backoff(&mut backoff, shutdown, promote);
break None; }
std::thread::yield_now();
};
let Some((lineage_start, lineage_anchor, lineage_genesis)) = stream_lineage else {
continue;
};
if pipeline.is_none() && journal_writer.is_none() {
let writer = match BufferedWriter::create_continuing(
store.journal(),
lineage_start,
lineage_anchor,
lineage_genesis,
) {
Ok(writer) => writer,
Err(e) => {
transport.reset(handle);
return Err(e.into());
}
};
app = Some(A::default());
journal_writer = Some(writer);
}
if pipeline.is_none() {
if app.is_none() || journal_writer.is_none() {
transport.reset(handle);
continue;
}
let cur_app = app.take().expect("application initialized");
let cur_writer = journal_writer.take().expect("journal_writer initialized");
if let Err(e) = melin_app::affinity::clear_affinity() {
tracing::warn!(error = e, "failed to clear receiver affinity before spawn");
}
pipeline = match build_replica_pipeline_with_threads::<A>(
cur_app,
cur_writer,
cores,
staging_mode,
snapshot_interval_ms,
store,
group_commit_delay,
Arc::clone(&fence_state),
lineage_ack_policy,
Arc::clone(pipeline_healthy),
sizing,
) {
Ok(p) => Some(p),
Err(e) => {
transport.reset(handle);
return Err(e);
}
};
melin_app::affinity::pin_thread("receiver", cores.reader.core);
}
let result = {
let p = pipeline.as_mut().expect("pipeline must exist by here");
let input_producer = &mut p.input_producer;
let journal_cursor = p.journal_cursor.as_ref();
let stream_marks = &p.stream_marks;
let journal_failed = &p.journal_failed;
let mut dpdk_transport = DpdkReceiverTransport {
transport: &mut *transport,
handle,
send_buf: std::mem::take(&mut send_buf),
};
let r = streaming_loop::<DpdkReceiverTransport<'_>, A::Event>(
&mut dpdk_transport,
input_producer,
journal_cursor,
shutdown,
control,
pipeline_depth,
cores.reader.wait,
last_sequence,
std::mem::take(&mut recv_buf),
None,
stream_marks,
journal_failed,
);
send_buf = dpdk_transport.send_buf;
r
};
primary_link_up.store(false, Ordering::Release);
let after = handle_session_exit(
result,
&mut pipeline,
&mut divergence_resyncs,
&mut backoff,
last_sequence,
store,
&fence_state,
lineage_ack_policy,
shutdown,
promote,
|| end_session(transport, handle),
sizing,
);
if let Some(r) = after.adopt(
&mut app,
&mut journal_writer,
&mut last_sequence,
&mut chain_hash,
) {
return r;
}
}
}
struct DpdkFrameSource<'a> {
transport: &'a mut melin_dpdk::DpdkTransport,
handle: melin_dpdk::SocketHandle,
recv_buf: &'a mut Vec<u8>,
shutdown: &'a AtomicBool,
}
impl ControlFrameSource for DpdkFrameSource<'_> {
fn next_frame(
&mut self,
max_size: usize,
) -> Result<Vec<u8>, Box<dyn std::error::Error + Send + Sync>> {
loop {
if self.shutdown.load(Ordering::Relaxed) {
return Err("shutdown during transfer".into());
}
self.transport.poll();
self.transport.recv_into_vec(self.handle, self.recv_buf);
match try_extract_frame(self.recv_buf, max_size) {
FrameResult::Complete(payload_start, frame_end) => {
let payload = self.recv_buf[payload_start..frame_end].to_vec();
compact_recv_buf(self.recv_buf, frame_end);
return Ok(payload);
}
FrameResult::Oversized => {
return Err("oversized frame during transfer".into());
}
FrameResult::Incomplete => {}
}
if !self.transport.is_connected(self.handle) {
return Err("disconnected during transfer".into());
}
std::thread::yield_now();
}
}
fn serviced<R: Send>(&mut self, work: impl FnOnce() -> R + Send) -> R {
run_serviced(work, || {
self.transport.poll();
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn frame(len: usize) -> Vec<u8> {
let mut f = (len as u32).to_le_bytes().to_vec();
f.resize(4 + len, 0xAB);
f
}
fn ack(acked_sequence: u64, in_memory_sequence: u64) -> Vec<u8> {
let mut buf = Vec::new();
encode_ack(
&Ack {
acked_sequence,
in_memory_sequence,
},
&mut buf,
);
buf
}
#[test]
fn keep_last_ack_keeps_the_newest_ack_and_a_partial_frame() {
let mut buf = ack(1, 2);
buf.extend(frame(8));
buf.extend(ack(3, 4));
buf.extend(frame(8));
let partial = ack(5, 6);
buf.extend_from_slice(&partial[..5]);
let acked = JoinFrames::Read { acked: true };
assert_eq!(keep_last_ack(&mut buf), acked);
let mut expected = ack(3, 4);
expected.extend_from_slice(&partial[..5]);
assert_eq!(buf, expected);
buf.extend_from_slice(&partial[5..]);
assert_eq!(keep_last_ack(&mut buf), acked);
assert_eq!(buf, ack(5, 6));
assert_eq!(keep_last_ack(&mut buf), acked, "idempotent");
assert_eq!(buf, ack(5, 6));
}
#[test]
fn keep_last_ack_with_no_ack_keeps_only_a_partial_frame() {
let none = JoinFrames::Read { acked: false };
let mut buf = frame(8);
buf.extend_from_slice(&frame(8)[..3]);
assert_eq!(keep_last_ack(&mut buf), none);
assert_eq!(buf, frame(8)[..3]);
let mut buf = Vec::new();
assert_eq!(keep_last_ack(&mut buf), none);
assert!(buf.is_empty());
let mut buf = vec![5, 0];
assert_eq!(keep_last_ack(&mut buf), none);
assert_eq!(buf, [5, 0]);
let partial = frame(12);
let mut buf = partial[..7].to_vec();
assert_eq!(keep_last_ack(&mut buf), none);
buf.extend_from_slice(&partial[7..]);
assert_eq!(keep_last_ack(&mut buf), none);
assert!(buf.is_empty());
}
#[test]
fn keep_last_ack_keeps_the_only_ack_until_the_join_is_over() {
let only = ack(1_560, 1_560);
let mut buf = only[..5].to_vec();
assert_eq!(keep_last_ack(&mut buf), JoinFrames::Read { acked: false });
buf.extend_from_slice(&only[5..]);
for read in 0..3 {
assert_eq!(
keep_last_ack(&mut buf),
JoinFrames::Read { acked: true },
"read {read}"
);
assert_eq!(buf, only, "read {read}");
}
}
#[test]
fn keep_last_ack_rejects_an_oversized_or_empty_frame() {
let mut buf = ack(1, 1);
buf.extend(((MAX_CONTROL_FRAME + 1) as u32).to_le_bytes());
assert_eq!(keep_last_ack(&mut buf), JoinFrames::Malformed);
let mut buf = 0u32.to_le_bytes().to_vec();
assert_eq!(keep_last_ack(&mut buf), JoinFrames::Malformed);
}
#[test]
fn the_join_stall_limit_is_the_liveness_timeout() {
assert_eq!(JOIN_STALL_LIMIT, REPLICATION_LIVENESS.timeout());
assert!(JOIN_STALL_LIMIT >= std::time::Duration::from_secs(1));
}
}