use std::collections::HashMap;
use std::net::SocketAddr;
use std::os::unix::io::{AsRawFd, FromRawFd, OwnedFd, RawFd};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc;
use std::thread::JoinHandle;
use std::time::{Duration, Instant};
use io_uring::{IoUring, opcode, types};
use tracing::{debug, error};
use crate::ControlEvent;
use crate::buf_ring::BufRing;
use crate::early_reply::EarlyReplySender;
use crate::halt::HaltGate;
use crate::policy_control::{AckPolicyPublisher, BootedClaim};
use melin_app::Application;
use melin_app::auth::{ClientRole, RoleId};
use melin_app::decoder::ErasedDecoder;
pub type RequestDecoderArc<A> = Arc<dyn ErasedDecoder<<A as Application>::Event>>;
use melin_app::unix_epoch_nanos;
use melin_pipeline::ring;
use melin_transport_core::health::IngressMetrics;
use melin_transport_core::pipeline::InputSlot;
use melin_transport_core::tick::TickSchedule;
const BUF_SIZE: usize = 4096;
const BUF_GROUP_ID: u16 = 0;
use crate::client_frames::{ClientIngress, FrameAction, LongParkReport, MAX_FRAME_SIZE};
use crate::connection_limit::{RingSizing, await_startup, ring_setup_error};
const EVENTFD_TOKEN: u64 = u64::MAX;
const PROVIDE_BUFS_TOKEN: u64 = u64::MAX - 1;
const CANCEL_TOKEN: u64 = u64::MAX - 3;
const TICK_TIMEOUT_TOKEN: u64 = u64::MAX - 2;
const PARK_RETRY_TOKEN: u64 = u64::MAX - 4;
const PARK_RETRY: Duration = Duration::from_millis(1);
const PARK_CANCEL_DEADLINE: Duration = Duration::from_secs(1);
const IORING_CQE_F_BUFFER: u32 = 1 << 0;
const IORING_CQE_F_MORE: u32 = 1 << 1;
const IORING_CQE_BUFFER_SHIFT: u32 = 16;
use melin_wire_protocol::control::ConnectionId;
pub struct ReaderRegistration<R> {
pub connection_id: ConnectionId,
pub reader: R,
pub addr: SocketAddr,
pub role: ClientRole<RoleId>,
pub key_hash: u64,
}
pub struct UringReaderHandle<R> {
tx: mpsc::Sender<ReaderRegistration<R>>,
event_fd: Arc<OwnedFd>,
join_handle: Option<JoinHandle<()>>,
shutdown: Arc<AtomicBool>,
}
impl<R> UringReaderHandle<R> {
pub fn register(&mut self, registration: ReaderRegistration<R>) {
if self.tx.send(registration).is_ok() {
wake_reader(&self.event_fd);
} else {
error!("reader thread dead, cannot register connection");
self.shutdown.store(true, Ordering::Relaxed);
}
}
pub fn shutdown(&self) {
self.shutdown.store(true, Ordering::Relaxed);
wake_reader(&self.event_fd);
}
pub fn join(mut self) {
if let Some(handle) = self.join_handle.take()
&& let Err(panic) = handle.join()
{
let msg = panic
.downcast_ref::<&str>()
.copied()
.or_else(|| panic.downcast_ref::<String>().map(|s| s.as_str()))
.unwrap_or("<non-string panic>");
error!(message = msg, "reader thread panicked");
}
}
}
pub(crate) fn wake_reader(event_fd: &OwnedFd) {
let val: u64 = 1;
unsafe {
libc::write(
event_fd.as_raw_fd(),
&val as *const u64 as *const libc::c_void,
8,
);
}
}
#[allow(clippy::too_many_arguments)]
pub fn spawn_reader<A: Application, R: AsRawFd + Send + 'static>(
mut producer: ring::Producer<InputSlot<A::Event>>,
decoder: RequestDecoderArc<A>,
halt: HaltGate,
early_replies: EarlyReplySender<A::Report>,
metrics: Arc<IngressMetrics>,
ack_policy: BootedClaim,
control_tx: mpsc::Sender<ControlEvent>,
core: usize,
connection_timeout: Option<Duration>,
tick_cadence: Option<Duration>,
sizing: RingSizing,
shutdown: Arc<AtomicBool>,
) -> std::io::Result<UringReaderHandle<R>>
where
A::Event: Send + Sync + 'static,
A::Report: Send + 'static,
{
let (tx, rx) = mpsc::channel();
let raw_event_fd = unsafe { libc::eventfd(0, libc::EFD_NONBLOCK) };
if raw_event_fd < 0 {
let err = std::io::Error::last_os_error();
return Err(std::io::Error::new(
err.kind(),
format!("uring-reader: cannot create its wakeup eventfd: {err}"),
));
}
let event_fd = Arc::new(unsafe { OwnedFd::from_raw_fd(raw_event_fd) });
let wakeup_fd = Arc::clone(&event_fd);
let shutdown_clone = Arc::clone(&shutdown);
let ack_policy = ack_policy.attach(&mut producer, &halt, Some(Arc::clone(&event_fd)));
let (ready_tx, ready_rx) = mpsc::sync_channel::<std::io::Result<()>>(1);
let handle = melin_app::affinity::spawn_placed("uring-reader", core, move || {
reader_loop::<A, R>(
rx,
wakeup_fd,
producer,
&*decoder,
&halt,
early_replies,
&metrics,
ack_policy,
&control_tx,
connection_timeout,
tick_cadence,
sizing,
ready_tx,
&shutdown_clone,
);
})
.map_err(|e| {
let e = std::io::Error::from(e);
std::io::Error::new(
e.kind(),
format!("uring-reader: cannot start its thread: {e}"),
)
})?;
if let Err(e) = await_startup("uring-reader", &ready_rx) {
let _ = handle.join();
return Err(e);
}
Ok(UringReaderHandle {
tx,
event_fd,
join_handle: Some(handle),
shutdown,
})
}
struct ConnectionEntry<R> {
connection_id: u64,
addr: SocketAddr,
role: ClientRole<RoleId>,
key_hash: u64,
_reader: R,
fd: RawFd,
ingress: ClientIngress,
recv: RecvState,
last_activity: Instant,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum RecvState {
Armed,
Idle,
Parked { cancelling: bool, since: Instant },
Resuming,
Dying,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum RecvCompletion {
Data,
Empty,
Closed,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum CqeStep {
Discard { free: bool },
Remove,
Process,
Keep,
Arm,
Nothing,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum FramesStep {
Nothing,
Arm,
Cancel,
Teardown,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum TeardownStep {
AlreadyDying,
FreeNow,
Cancel,
AwaitCancel,
}
impl RecvState {
fn on_cqe(self, completion: RecvCompletion, terminal: bool) -> (RecvState, CqeStep) {
if self == RecvState::Dying {
return (self, CqeStep::Discard { free: terminal });
}
let after = if terminal {
self.after_terminal()
} else {
self
};
match completion {
RecvCompletion::Closed => (after, CqeStep::Remove),
RecvCompletion::Empty => match after {
RecvState::Idle => (RecvState::Armed, CqeStep::Arm),
_ => (after, CqeStep::Nothing),
},
RecvCompletion::Data => match after {
RecvState::Parked { .. } => (after, CqeStep::Keep),
_ => (after, CqeStep::Process),
},
}
}
fn after_terminal(self) -> RecvState {
match self {
RecvState::Armed | RecvState::Resuming => RecvState::Idle,
RecvState::Parked { since, .. } => RecvState::Parked {
cancelling: false,
since,
},
RecvState::Idle | RecvState::Dying => self,
}
}
fn after_frames(self, action: FrameAction, now: Instant) -> (RecvState, FramesStep) {
match action {
FrameAction::Disconnect => (self, FramesStep::Teardown),
FrameAction::Continue => match self {
RecvState::Armed | RecvState::Resuming | RecvState::Dying => {
(self, FramesStep::Nothing)
}
RecvState::Parked {
cancelling: true, ..
} => (RecvState::Resuming, FramesStep::Nothing),
RecvState::Idle
| RecvState::Parked {
cancelling: false, ..
} => (RecvState::Armed, FramesStep::Arm),
},
FrameAction::Park => match self {
RecvState::Armed => (
RecvState::Parked {
cancelling: true,
since: now,
},
FramesStep::Cancel,
),
RecvState::Resuming => (
RecvState::Parked {
cancelling: true,
since: now,
},
FramesStep::Nothing,
),
RecvState::Idle => (
RecvState::Parked {
cancelling: false,
since: now,
},
FramesStep::Nothing,
),
RecvState::Parked { .. } | RecvState::Dying => (self, FramesStep::Nothing),
},
}
}
fn on_teardown(self) -> (RecvState, TeardownStep) {
match self {
RecvState::Dying => (self, TeardownStep::AlreadyDying),
RecvState::Idle
| RecvState::Parked {
cancelling: false, ..
} => (self, TeardownStep::FreeNow),
RecvState::Armed => (RecvState::Dying, TeardownStep::Cancel),
RecvState::Resuming
| RecvState::Parked {
cancelling: true, ..
} => (RecvState::Dying, TeardownStep::AwaitCancel),
}
}
fn park_cancel_overdue(self, now: Instant) -> bool {
match self {
RecvState::Parked {
cancelling: true,
since,
} => now.duration_since(since) > PARK_CANCEL_DEADLINE,
RecvState::Armed
| RecvState::Idle
| RecvState::Resuming
| RecvState::Dying
| RecvState::Parked {
cancelling: false, ..
} => false,
}
}
fn kernel_owes_cqe(self) -> bool {
match self {
RecvState::Armed
| RecvState::Resuming
| RecvState::Dying
| RecvState::Parked {
cancelling: true, ..
} => true,
RecvState::Idle
| RecvState::Parked {
cancelling: false, ..
} => false,
}
}
}
struct ConnectionSlab<R> {
entries: Vec<Option<ConnectionEntry<R>>>,
free: Vec<usize>,
}
impl<R> ConnectionSlab<R> {
fn new() -> Self {
Self {
entries: Vec::with_capacity(256),
free: Vec::new(),
}
}
fn insert(&mut self, entry: ConnectionEntry<R>) -> usize {
if let Some(idx) = self.free.pop() {
self.entries[idx] = Some(entry);
idx
} else {
let idx = self.entries.len();
self.entries.push(Some(entry));
idx
}
}
fn get_mut(&mut self, idx: usize) -> Option<&mut ConnectionEntry<R>> {
self.entries.get_mut(idx).and_then(|e| e.as_mut())
}
fn remove(&mut self, idx: usize) -> Option<ConnectionEntry<R>> {
if let Some(slot) = self.entries.get_mut(idx) {
let removed = slot.take();
if removed.is_some() {
self.free.push(idx);
}
removed
} else {
None
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum RingEntry {
Skip,
Submit,
SubmitAndWait,
}
#[inline]
fn ring_entry(sq_pending: usize, cq_ready: bool) -> RingEntry {
match (sq_pending > 0, cq_ready) {
(false, true) => RingEntry::Skip,
(true, true) => RingEntry::Submit,
(_, false) => RingEntry::SubmitAndWait,
}
}
#[allow(clippy::too_many_arguments)]
fn reader_loop<A: Application, R: AsRawFd>(
command_rx: mpsc::Receiver<ReaderRegistration<R>>,
wakeup_fd: Arc<OwnedFd>,
mut producer: ring::Producer<InputSlot<A::Event>>,
decoder: &dyn ErasedDecoder<A::Event>,
halt: &HaltGate,
mut early_replies: EarlyReplySender<A::Report>,
metrics: &IngressMetrics,
mut ack_policy: AckPolicyPublisher,
control_tx: &mpsc::Sender<ControlEvent>,
connection_timeout: Option<Duration>,
tick_cadence: Option<Duration>,
sizing: RingSizing,
ready: mpsc::SyncSender<std::io::Result<()>>,
shutdown: &AtomicBool,
) {
let num_buffers = sizing.provided_buffers();
let ring_entries = sizing.ring_entries();
let mut eventfd_buf: Box<[u8; 8]> = Box::new([0u8; 8]);
let mut buffer_pool = vec![0u8; num_buffers as usize * BUF_SIZE].into_boxed_slice();
let mut buf_ring = BufRing::new(num_buffers, buffer_pool.as_mut_ptr(), BUF_SIZE);
let mut ring: IoUring = match IoUring::builder().setup_single_issuer().build(ring_entries) {
Ok(ring) => ring,
Err(e) => {
let _ = ready.send(Err(ring_setup_error("uring-reader", ring_entries, e)));
return;
}
};
let use_buf_ring = match buf_ring.register(&ring, BUF_GROUP_ID) {
Ok(()) => true,
Err(e) => {
tracing::warn!(
error = %e,
"buf_ring registration rejected — falling back to legacy \
ProvideBuffers recycling (kernel < 5.19, or a hypervisor \
filtering io_uring register opcodes)"
);
if let Err(e) = register_buffer_pool(&mut ring, buffer_pool.as_mut_ptr(), num_buffers) {
let _ = ready.send(Err(std::io::Error::new(
e.kind(),
format!("uring-reader: cannot provide its receive buffers: {e}"),
)));
return;
}
false
}
};
if ready.send(Ok(())).is_err() {
return;
}
let mut slab = ConnectionSlab::<R>::new();
let mut fd_to_slab: HashMap<RawFd, usize> = HashMap::with_capacity(256);
let mut cqes: Vec<(u64, i32, u32)> = Vec::with_capacity(ring_entries as usize * 2);
push_eventfd_read(&mut ring, wakeup_fd.as_raw_fd(), eventfd_buf.as_mut_ptr());
let mut eventfd_armed = true;
#[cfg(feature = "latency-trace")]
let mut publish_rec =
melin_transport_core::trace::register_stage("reader: publish (decode → disruptor publish)");
#[cfg(feature = "tick-to-trade")]
let mut ingest_rec =
melin_transport_core::trace::register_stage("reader: ingest (recv_ts → publish complete)");
#[cfg(feature = "latency-trace")]
let mut last_stats_flush = Instant::now();
let mut last_timeout_scan = Instant::now();
let mut stale: Vec<usize> = Vec::new();
let mut any_parked = false;
let mut park_retry_armed = false;
let park_retry_ts = types::Timespec::new()
.sec(PARK_RETRY.as_secs())
.nsec(PARK_RETRY.subsec_nanos());
let mut long_park_report = LongParkReport::default();
let tick_enabled = tick_cadence.is_some();
let cadence = tick_cadence.unwrap_or(Duration::ZERO);
let mut tick_schedule = TickSchedule::new(cadence, Instant::now());
let mut tick_armed = false;
let mut tick_ts = types::Timespec::new()
.sec(cadence.as_secs())
.nsec(cadence.subsec_nanos());
if tick_enabled {
let sqe = opcode::Timeout::new(&tick_ts)
.build()
.user_data(TICK_TIMEOUT_TOKEN);
unsafe {
ring.submission()
.push(&sqe)
.expect("io_uring SQ full while arming initial tick timeout");
}
tick_armed = true;
tracing::info!(
cadence_ms = cadence.as_millis() as u64,
"tick generator integrated into reader thread"
);
}
loop {
if shutdown.load(Ordering::Relaxed) {
break;
}
ack_policy.poll(&mut producer, halt);
if tick_enabled {
if tick_schedule.publish_if_due(Instant::now(), unix_epoch_nanos, &mut producer) {
tick_armed = false;
}
if !tick_armed {
let remaining = tick_schedule
.next_deadline()
.saturating_duration_since(Instant::now());
tick_ts = types::Timespec::new()
.sec(remaining.as_secs())
.nsec(remaining.subsec_nanos());
let sqe = opcode::Timeout::new(&tick_ts)
.build()
.user_data(TICK_TIMEOUT_TOKEN);
unsafe { push_sqe(&mut ring, &sqe, "the tick TIMEOUT") };
tick_armed = true;
}
}
if any_parked && !park_retry_armed {
let sqe = opcode::Timeout::new(&park_retry_ts)
.build()
.user_data(PARK_RETRY_TOKEN);
unsafe { push_sqe(&mut ring, &sqe, "the park-retry TIMEOUT") };
park_retry_armed = true;
}
let sq_pending = ring.submission().len();
let cq_ready = !ring.completion().is_empty();
let entered = match ring_entry(sq_pending, cq_ready) {
RingEntry::Skip => Ok(0),
RingEntry::Submit => ring.submit(),
RingEntry::SubmitAndWait => ring.submit_and_wait(1),
};
match entered {
Ok(_) => {}
Err(ref e) if e.raw_os_error() == Some(libc::EINTR) => continue,
Err(e) => {
error!(error = %e, "io_uring submit/wait error");
break;
}
}
cqes.clear();
cqes.extend(
ring.completion()
.map(|cqe| (cqe.user_data(), cqe.result(), cqe.flags())),
);
let batch_now = Instant::now();
let batch_wall_ns = unix_epoch_nanos();
for &(token, result, flags) in &cqes {
if token == TICK_TIMEOUT_TOKEN {
tick_armed = false;
continue;
}
if token == PARK_RETRY_TOKEN {
park_retry_armed = false;
continue;
}
if token == PROVIDE_BUFS_TOKEN {
if result < 0 {
error!(error = result, "ProvideBuffers failed");
}
continue;
}
if token == CANCEL_TOKEN {
continue;
}
if token == EVENTFD_TOKEN {
if result >= 0 {
while let Ok(reg) = command_rx.try_recv() {
let fd = reg.reader.as_raw_fd();
let entry = ConnectionEntry {
connection_id: reg.connection_id.0,
addr: reg.addr,
role: reg.role,
key_hash: reg.key_hash,
fd,
_reader: reg.reader,
ingress: ClientIngress::new(Vec::with_capacity(MAX_FRAME_SIZE + 4)),
recv: RecvState::Armed,
last_activity: Instant::now(),
};
let idx = slab.insert(entry);
fd_to_slab.insert(fd, idx);
push_recv_multi(&mut ring, fd, idx);
}
} else {
error!(error = result, "eventfd read error");
}
push_eventfd_read(&mut ring, wakeup_fd.as_raw_fd(), eventfd_buf.as_mut_ptr());
continue;
}
let slab_idx = token as usize;
let terminal = (flags & IORING_CQE_F_MORE) == 0;
let buf_id = if (flags & IORING_CQE_F_BUFFER) != 0 {
Some((flags >> IORING_CQE_BUFFER_SHIFT) as usize)
} else {
None
};
let Some(entry) = slab.get_mut(slab_idx) else {
if let Some(bid) = buf_id {
recycle_buffer(&mut ring, &mut buf_ring, use_buf_ring, &buffer_pool, bid);
}
continue;
};
let completion = if result == -libc::ENOBUFS || result == -libc::ECANCELED {
RecvCompletion::Empty
} else if result <= 0 {
RecvCompletion::Closed
} else if buf_id.is_none() {
debug!(slab_idx, "recv CQE without buffer flag");
RecvCompletion::Empty
} else {
RecvCompletion::Data
};
let (recv, step) = entry.recv.on_cqe(completion, terminal);
entry.recv = recv;
let fd = entry.fd;
let (Some(buf_id), CqeStep::Process | CqeStep::Keep) = (buf_id, step) else {
if let Some(bid) = buf_id {
recycle_buffer(&mut ring, &mut buf_ring, use_buf_ring, &buffer_pool, bid);
}
match step {
CqeStep::Discard { free } => {
if free && let Some(dead) = slab.remove(slab_idx) {
debug!(
connection_id = dead.connection_id,
"teardown complete, slab index released"
);
}
}
CqeStep::Remove => {
if let Some(removed) = slab.remove(slab_idx) {
if result == 0 {
debug!(
connection_id = removed.connection_id,
addr = %removed.addr,
"client disconnected"
);
} else {
debug!(
connection_id = removed.connection_id,
addr = %removed.addr,
error = result,
"recv error"
);
}
fd_to_slab.remove(&removed.fd);
let _ = control_tx.send(ControlEvent::Disconnected {
connection_id: removed.connection_id,
});
}
}
CqeStep::Arm => push_recv_multi(&mut ring, fd, slab_idx),
CqeStep::Nothing | CqeStep::Process | CqeStep::Keep => {}
}
continue;
};
let n = result as usize;
#[allow(clippy::let_unit_value)] let recv_ts = melin_transport_core::trace::mono_trace_ns();
entry.last_activity = batch_now;
let buf_start = buf_id * BUF_SIZE;
entry
.ingress
.extend_from_slice(&buffer_pool[buf_start..buf_start + n]);
recycle_buffer(&mut ring, &mut buf_ring, use_buf_ring, &buffer_pool, buf_id);
if step == CqeStep::Keep {
continue;
}
let action = process_frames::<A, R>(
entry,
&mut producer,
decoder,
halt,
&mut early_replies,
metrics,
batch_wall_ns,
recv_ts,
#[cfg(feature = "latency-trace")]
&mut publish_rec,
#[cfg(feature = "tick-to-trade")]
&mut ingest_rec,
);
let (recv, step) = entry.recv.after_frames(action, batch_now);
entry.recv = recv;
if matches!(recv, RecvState::Parked { .. }) {
any_parked = true;
metrics.record_park();
debug!(connection_id = entry.connection_id, "connection parked");
}
apply_frames_step(
&mut ring,
&mut slab,
&mut fd_to_slab,
control_tx,
slab_idx,
step,
);
}
if any_parked {
any_parked = false;
let mut still_parked = 0u64;
let mut long_parked = 0usize;
#[allow(clippy::let_unit_value)] let recv_ts = melin_transport_core::trace::mono_trace_ns();
for idx in 0..slab.entries.len() {
let Some(entry) = slab.get_mut(idx) else {
continue;
};
let RecvState::Parked { since, .. } = entry.recv else {
continue;
};
if entry.recv.park_cancel_overdue(batch_now) {
tracing::warn!(
connection_id = entry.connection_id,
"a parked connection's receive cancel went unanswered (is io_uring \
AsyncCancel filtered on this host?): closing the connection"
);
begin_teardown(&mut ring, &mut slab, &mut fd_to_slab, control_tx, idx);
continue;
}
let action = process_frames::<A, R>(
entry,
&mut producer,
decoder,
halt,
&mut early_replies,
metrics,
batch_wall_ns,
recv_ts,
#[cfg(feature = "latency-trace")]
&mut publish_rec,
#[cfg(feature = "tick-to-trade")]
&mut ingest_rec,
);
let (recv, step) = entry.recv.after_frames(action, batch_now);
entry.recv = recv;
match action {
FrameAction::Park => {
any_parked = true;
still_parked += 1;
if LongParkReport::parked_long(since, batch_now) {
long_parked += 1;
}
}
FrameAction::Continue => {
entry.last_activity = batch_now;
debug!(connection_id = entry.connection_id, "connection resumed");
}
FrameAction::Disconnect => {}
}
apply_frames_step(&mut ring, &mut slab, &mut fd_to_slab, control_tx, idx, step);
}
metrics.set_parked(still_parked);
long_park_report.report(long_parked, batch_now);
}
if let Some(timeout) = connection_timeout {
let now = Instant::now();
if now.duration_since(last_timeout_scan) >= Duration::from_secs(1) {
last_timeout_scan = now;
stale.clear();
for (idx, slot) in slab.entries.iter().enumerate() {
if let Some(entry) = slot
&& !matches!(entry.recv, RecvState::Dying | RecvState::Parked { .. })
&& now.duration_since(entry.last_activity) > timeout
{
debug!(
connection_id = entry.connection_id,
addr = %entry.addr,
"connection timed out"
);
stale.push(idx);
}
}
for &idx in &stale {
begin_teardown(&mut ring, &mut slab, &mut fd_to_slab, control_tx, idx);
}
}
}
#[cfg(feature = "latency-trace")]
if batch_now.duration_since(last_stats_flush)
>= melin_transport_core::trace::IDLE_FLUSH_INTERVAL
{
last_stats_flush = batch_now;
publish_rec.flush();
#[cfg(feature = "tick-to-trade")]
ingest_rec.flush();
}
}
let proven = {
let cancel_all = opcode::AsyncCancel2::new(types::CancelBuilder::any())
.build()
.user_data(CANCEL_TOKEN);
unsafe {
let _ = ring.submission().push(&cancel_all);
}
for entry in slab.entries.iter().flatten() {
crate::uring_teardown::wake_pending_ops(entry.fd);
}
let wake: u64 = 1;
unsafe {
libc::write(
wakeup_fd.as_raw_fd(),
&wake as *const u64 as *const libc::c_void,
8,
);
}
loop {
match ring.submit() {
Ok(_) => break,
Err(ref e) if e.raw_os_error() == Some(libc::EINTR) => continue,
Err(_) => break,
}
}
let mut backoff = crate::uring_teardown::DrainBackoff::new();
loop {
for cqe in ring.completion() {
match cqe.user_data() {
EVENTFD_TOKEN => eventfd_armed = false,
PROVIDE_BUFS_TOKEN | CANCEL_TOKEN | TICK_TIMEOUT_TOKEN | PARK_RETRY_TOKEN => {}
idx => {
if (cqe.flags() & IORING_CQE_F_MORE) == 0
&& let Some(entry) = slab.get_mut(idx as usize)
{
entry.recv = RecvState::Idle;
}
}
}
}
let owed = eventfd_armed
|| slab
.entries
.iter()
.flatten()
.any(|e| e.recv.kernel_owes_cqe());
if !owed {
break true;
}
if !backoff.wait() {
break false;
}
}
};
if !proven {
tracing::warn!(
eventfd_armed,
armed_connections = slab
.entries
.iter()
.flatten()
.filter(|e| e.recv.kernel_owes_cqe())
.count(),
"io_uring teardown drain did not complete; leaking reader buffers"
);
std::mem::forget(buffer_pool);
std::mem::forget(buf_ring);
std::mem::forget(eventfd_buf);
}
}
unsafe fn push_sqe(ring: &mut IoUring, sqe: &io_uring::squeue::Entry, what: &str) {
if unsafe { ring.submission().push(sqe) }.is_ok() {
return;
}
loop {
match ring.submit() {
Ok(_) => break,
Err(ref e) if e.raw_os_error() == Some(libc::EINTR) => continue,
Err(e) => panic!("io_uring submit failed while making room for {what}: {e}"),
}
}
unsafe { ring.submission().push(sqe) }.unwrap_or_else(|_| {
panic!("io_uring SQ still full for {what} after submitting every queued SQE")
});
}
fn register_buffer_pool(
ring: &mut IoUring,
pool_ptr: *mut u8,
num_buffers: u16,
) -> std::io::Result<()> {
let sqe = opcode::ProvideBuffers::new(pool_ptr, BUF_SIZE as i32, num_buffers, BUF_GROUP_ID, 0)
.build()
.user_data(PROVIDE_BUFS_TOKEN);
unsafe {
ring.submission().push(&sqe).map_err(|_| {
std::io::Error::other("io_uring SQ full during buffer pool registration")
})?;
}
loop {
match ring.submit_and_wait(1) {
Ok(_) => break,
Err(ref e) if e.raw_os_error() == Some(libc::EINTR) => continue,
Err(e) => return Err(e),
}
}
let cqe = ring
.completion()
.next()
.ok_or_else(|| std::io::Error::other("no CQE after ProvideBuffers"))?;
if cqe.result() < 0 {
return Err(std::io::Error::from_raw_os_error(-cqe.result()));
}
Ok(())
}
fn re_provide_buffer(ring: &mut IoUring, pool_ptr: *mut u8, buf_id: usize) {
let buf_ptr = unsafe { pool_ptr.add(buf_id * BUF_SIZE) };
let sqe = opcode::ProvideBuffers::new(buf_ptr, BUF_SIZE as i32, 1, BUF_GROUP_ID, buf_id as u16)
.build()
.user_data(PROVIDE_BUFS_TOKEN);
unsafe { push_sqe(ring, &sqe, "a legacy ProvideBuffers recycle") };
}
fn recycle_buffer(
ring: &mut IoUring,
buf_ring: &mut BufRing,
use_buf_ring: bool,
buffer_pool: &[u8],
buf_id: usize,
) {
assert!(
buf_id < buffer_pool.len() / BUF_SIZE,
"kernel returned out-of-pool buffer id {buf_id}"
);
if use_buf_ring {
buf_ring.push(buf_id as u16);
} else {
re_provide_buffer(ring, buffer_pool.as_ptr() as *mut u8, buf_id);
}
}
fn begin_teardown<R>(
ring: &mut IoUring,
slab: &mut ConnectionSlab<R>,
fd_to_slab: &mut HashMap<RawFd, usize>,
control_tx: &mpsc::Sender<ControlEvent>,
idx: usize,
) {
let Some(entry) = slab.get_mut(idx) else {
return;
};
let (recv, step) = entry.recv.on_teardown();
if step == TeardownStep::AlreadyDying {
return;
}
entry.recv = recv;
let connection_id = entry.connection_id;
let fd = entry.fd;
unsafe {
libc::shutdown(fd, libc::SHUT_RDWR);
}
fd_to_slab.remove(&fd);
let _ = control_tx.send(ControlEvent::Disconnected { connection_id });
match step {
TeardownStep::Cancel => push_recv_cancel(ring, idx, "a connection teardown's AsyncCancel"),
TeardownStep::AwaitCancel => {}
TeardownStep::FreeNow => {
slab.remove(idx);
}
TeardownStep::AlreadyDying => {}
}
}
fn apply_frames_step<R>(
ring: &mut IoUring,
slab: &mut ConnectionSlab<R>,
fd_to_slab: &mut HashMap<RawFd, usize>,
control_tx: &mpsc::Sender<ControlEvent>,
idx: usize,
step: FramesStep,
) {
match step {
FramesStep::Nothing => {}
FramesStep::Arm => {
if let Some(entry) = slab.get_mut(idx) {
push_recv_multi(ring, entry.fd, idx);
}
}
FramesStep::Cancel => push_recv_cancel(ring, idx, "a park's AsyncCancel"),
FramesStep::Teardown => begin_teardown(ring, slab, fd_to_slab, control_tx, idx),
}
}
fn push_recv_multi(ring: &mut IoUring, fd: RawFd, idx: usize) {
let sqe = opcode::RecvMulti::new(types::Fd(fd), BUF_GROUP_ID)
.build()
.user_data(idx as u64);
unsafe { push_sqe(ring, &sqe, "a multishot RECV") };
}
fn push_recv_cancel(ring: &mut IoUring, idx: usize, what: &str) {
let sqe = opcode::AsyncCancel::new(idx as u64)
.build()
.user_data(CANCEL_TOKEN);
unsafe { push_sqe(ring, &sqe, what) };
}
fn push_eventfd_read(ring: &mut IoUring, wakeup_fd: RawFd, buf: *mut u8) {
let sqe = opcode::Read::new(types::Fd(wakeup_fd), buf, 8)
.build()
.user_data(EVENTFD_TOKEN);
unsafe { push_sqe(ring, &sqe, "the eventfd READ") };
}
#[allow(clippy::too_many_arguments)]
fn process_frames<A: Application, R>(
conn: &mut ConnectionEntry<R>,
producer: &mut ring::Producer<InputSlot<A::Event>>,
decoder: &dyn ErasedDecoder<A::Event>,
halt: &HaltGate,
early_replies: &mut EarlyReplySender<A::Report>,
metrics: &IngressMetrics,
batch_wall_ns: u64,
recv_ts: melin_transport_core::trace::MonoTraceInstant,
#[cfg(feature = "latency-trace")] publish_rec: &mut melin_transport_core::trace::StageRecorder,
#[cfg(feature = "tick-to-trade")] ingest_rec: &mut melin_transport_core::trace::StageRecorder,
) -> FrameAction {
crate::client_frames::process_client_frames::<A>(
&mut conn.ingress,
conn.connection_id,
conn.key_hash,
conn.role,
producer,
decoder,
halt,
early_replies,
metrics,
batch_wall_ns,
recv_ts,
#[cfg(feature = "latency-trace")]
publish_rec,
#[cfg(feature = "tick-to-trade")]
ingest_rec,
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::client_frames::MAX_REQUEST_BODY;
use crate::early_reply::{EarlyKind, EarlyReply};
use melin_app::auth::NoRoles;
use melin_app::decoder::{Decoded, RequestDecoder};
use melin_app::{AppEvent, Application, ApplyCtx, CodecError, QueryCtx, RejectReason};
use melin_journal::JournalEvent;
use melin_pipeline::ring::DisruptorBuilder;
use melin_transport_core::health::IngressTally;
use melin_transport_core::pipeline::RequestId;
use melin_wire_protocol::control::RefusalKind;
use melin_wire_protocol::control_codec::{
TAG_APP, TAG_CHALLENGE_RESPONSE, TAG_LEN, TAG_RESPONSE_HEARTBEAT,
};
use std::io::{ErrorKind, Read};
use std::os::unix::net::UnixStream;
use std::sync::atomic::AtomicU64;
use std::time::Duration;
mod ring_entry {
use super::super::{RingEntry, ring_entry};
#[test]
fn visible_completions_and_nothing_to_submit_need_no_syscall() {
assert_eq!(ring_entry(0, true), RingEntry::Skip);
}
#[test]
fn queued_sqes_are_always_handed_over() {
assert_eq!(ring_entry(1, true), RingEntry::Submit);
assert_eq!(ring_entry(8, false), RingEntry::SubmitAndWait);
}
#[test]
fn an_empty_cq_is_the_only_thing_that_parks_the_thread() {
assert_eq!(ring_entry(0, false), RingEntry::SubmitAndWait);
for sq in [0, 1, 64] {
assert_ne!(ring_entry(sq, true), RingEntry::SubmitAndWait);
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum TestEvent {
Cmd(u8),
Query,
}
impl AppEvent for TestEvent {
const MAX_ENCODED_SIZE: usize = 2;
fn encoded_size(&self) -> usize {
2
}
fn encode(&self, _buf: &mut [u8]) -> usize {
unreachable!("process_frames does not encode app events")
}
fn decode(_buf: &[u8]) -> Result<Self, CodecError> {
unreachable!("process_frames does not decode app events directly")
}
fn is_query(&self) -> bool {
matches!(self, TestEvent::Query)
}
}
#[derive(Default)]
struct TestApp;
type TestReport = (TestEvent, RejectReason);
impl Application for TestApp {
type Event = TestEvent;
type Report = TestReport;
type QueryResponse = ();
type Sizing = ();
const APP_VERSION: u16 = 0;
fn apply(&mut self, _event: TestEvent, _ctx: &ApplyCtx, _out: &mut Vec<TestReport>) {
unreachable!()
}
fn query(&self, _event: TestEvent, _ctx: &QueryCtx) -> Option<()> {
unreachable!()
}
fn tick(&mut self, _now_ns: u64, _out: &mut Vec<TestReport>) {
unreachable!()
}
fn build_reject(event: &TestEvent, reason: RejectReason) -> TestReport {
(*event, reason)
}
fn snapshot<W: std::io::Write>(&self, _w: &mut W) -> std::io::Result<()> {
unreachable!()
}
fn restore<R: Read>(_r: &mut R) -> std::io::Result<Self> {
unreachable!()
}
}
struct ByteDecoder;
impl RequestDecoder for ByteDecoder {
type Event = TestEvent;
type Role = NoRoles;
fn decode(&self, body: &[u8], _role: ClientRole<NoRoles>) -> Decoded<TestEvent> {
match body.first().copied() {
None => Decoded::DecodeError("empty body"),
Some(0xFC) => Decoded::Filter,
Some(0xFD) => Decoded::PermissionDenied("denied"),
Some(0xFE) => Decoded::DecodeError("bad"),
Some(0xFF) => Decoded::Permitted(TestEvent::Query),
Some(b) => Decoded::Permitted(TestEvent::Cmd(b)),
}
}
}
fn tagged_frame(tag: u8, rest: &[u8]) -> Vec<u8> {
let len = (TAG_LEN + rest.len()) as u32;
let mut f = Vec::with_capacity(4 + len as usize);
f.extend_from_slice(&len.to_le_bytes());
f.push(tag);
f.extend_from_slice(rest);
f
}
fn request_frame(id: u64, body: &[u8]) -> Vec<u8> {
let mut buf = vec![0u8; melin_wire_protocol::framing::REQUEST_HEADER_LEN + body.len()];
let n = melin_wire_protocol::framing::frame_request(&mut buf, RequestId(id), |out| {
out[..body.len()].copy_from_slice(body);
Ok::<_, melin_wire_protocol::framing::RequestFrameError>(body.len())
})
.expect("a test request fits");
buf.truncate(n);
buf
}
fn id_of(byte: u8) -> RequestId {
RequestId(0x1_0000 + u64::from(byte))
}
fn frame(byte: u8) -> Vec<u8> {
request_frame(id_of(byte).0, &[byte])
}
fn oversize_prefix() -> [u8; 4] {
((MAX_FRAME_SIZE as u32) + 1).to_le_bytes()
}
struct Fixture {
conn: ConnectionEntry<UnixStream>,
producer: ring::Producer<InputSlot<TestEvent>>,
consumer: ring::Consumer<InputSlot<TestEvent>>,
peer: UnixStream,
}
fn make_fixture(ring_capacity: usize) -> Fixture {
let (server_side, peer) = UnixStream::pair().expect("UnixStream::pair");
peer.set_read_timeout(Some(Duration::from_millis(50)))
.expect("set_read_timeout");
let entry = ConnectionEntry::<UnixStream> {
connection_id: 7,
addr: "127.0.0.1:1".parse().expect("addr parses"),
role: ClientRole::Operator,
key_hash: 0xC0FFEE_u64,
fd: server_side.as_raw_fd(),
_reader: server_side,
ingress: ClientIngress::new(Vec::with_capacity(64)),
recv: RecvState::Idle,
last_activity: Instant::now(),
};
let (producer, mut consumers) =
DisruptorBuilder::<InputSlot<TestEvent>>::new(ring_capacity)
.add_consumer()
.build(melin_pipeline::wait::WaitStrategy::SpinThenYield);
let consumer = consumers.pop().expect("consumer present");
Fixture {
conn: entry,
producer,
consumer,
peer,
}
}
fn run_process_frames(
conn: &mut ConnectionEntry<UnixStream>,
producer: &mut ring::Producer<InputSlot<TestEvent>>,
) -> (FrameAction, Vec<EarlyReply<TestReport>>) {
let (mut early, mut queue) = early_channel();
let action = run_process_frames_with(conn, producer, &gate(None, false), &mut early);
(action, released(&mut queue))
}
fn released(
queue: &mut crate::early_reply::EarlyReplyQueue<TestReport>,
) -> Vec<EarlyReply<TestReport>> {
let boundary = crate::batch_boundary::BatchBoundary::new();
let at = boundary.at_boundary().expect("between events");
let mut out = Vec::new();
queue.register(|| {}).drain(&at, |reply| out.push(reply));
out
}
fn early_reply(id: RequestId, kind: EarlyKind<TestReport>) -> EarlyReply<TestReport> {
EarlyReply {
connection_id: 7,
request_id: id,
kind,
}
}
fn gate(replicas: Option<u32>, fenced: bool) -> HaltGate {
gate_counting_into(replicas, fenced, Arc::new(AtomicU64::new(0)))
}
fn gate_counting_into(
replicas: Option<u32>,
fenced: bool,
refused: Arc<AtomicU64>,
) -> HaltGate {
let fence = Arc::new(melin_transport_core::fence::FenceState::new(0));
if fenced {
fence.fence();
}
HaltGate::new(
replicas.map(crate::halt::test_support::presence),
fence,
refused,
)
}
fn refused_count(refused: &AtomicU64) -> u64 {
refused.load(std::sync::atomic::Ordering::Relaxed)
}
fn policy_claim() -> crate::policy_control::BootedClaim {
crate::policy_control::test_support::booted(
&crate::policy_control::test_support::replicated(crate::ack_policy::AckPolicy::Disk),
)
}
#[test]
fn a_parked_reader_journals_an_ack_policy_request() {
use crate::ack_policy::AckPolicy;
use crate::halt::Verdict;
use crate::halt::test_support::{gate_over, presence};
use crate::policy_control::RequestOutcome;
let replicas = presence(0);
let halt = || gate_over(Some(Arc::clone(&replicas)));
assert_eq!(
halt().verdict(),
Verdict::Refuse(RejectReason::ReplicaDisconnected)
);
let (handle, mut consumer, control) = spawn_parked_reader(halt(), None);
assert_eq!(control.request(AckPolicy::Disk), RequestOutcome::Published);
let (_, slot) = consumer.try_consume().expect("the entry is on the ring");
assert_eq!(
slot.event,
JournalEvent::AckPolicyChange(melin_journal::AckPolicyChange {
policy: AckPolicy::Disk.to_byte(),
unwitnessed: true,
})
);
assert_eq!(halt().verdict(), Verdict::Take);
handle.shutdown();
handle.join();
assert_eq!(
control.request(AckPolicy::Ram),
RequestOutcome::Staged,
"a stopped reader is detached"
);
}
#[test]
fn a_reader_journals_a_policy_staged_during_bring_up_first() {
use crate::ack_policy::AckPolicy;
use crate::halt::test_support::{gate_over, presence};
use crate::policy_control::RequestOutcome;
let (handle, mut consumer, control) =
spawn_parked_reader(gate_over(Some(presence(0))), Some(AckPolicy::Disk));
assert_eq!(control.request(AckPolicy::Disk), RequestOutcome::Published);
handle.shutdown();
handle.join();
let mut changes = Vec::new();
while let Some((_, slot)) = consumer.try_consume() {
if let JournalEvent::AckPolicyChange(change) = slot.event {
changes.push(change.unwitnessed);
}
}
assert_eq!(
changes,
[false, true],
"staged first, then the forced runtime one"
);
}
fn spawn_parked_reader(
halt: HaltGate,
staged: Option<crate::ack_policy::AckPolicy>,
) -> (
UringReaderHandle<UnixStream>,
ring::Consumer<InputSlot<TestEvent>>,
Arc<crate::policy_control::AckPolicyControl>,
) {
use crate::ack_policy::AckPolicy;
use crate::policy_control::RequestOutcome;
use crate::policy_control::test_support::{control_with_publish_wait, first_change};
let (mut producer, mut consumers) = DisruptorBuilder::<InputSlot<TestEvent>>::new(64)
.add_consumer()
.build(melin_pipeline::wait::WaitStrategy::SpinThenYield);
let consumer = consumers.pop().expect("consumer");
let (control_tx, _) = mpsc::channel();
let control = control_with_publish_wait(AckPolicy::DiskAndRam, Duration::from_secs(30));
let claim =
control
.claim()
.journal_boot(first_change(AckPolicy::DiskAndRam), &mut producer, &halt);
if let Some(policy) = staged {
assert_eq!(control.request(policy), RequestOutcome::Staged);
}
let handle = spawn_reader::<TestApp, UnixStream>(
producer,
Arc::new(ByteDecoder),
halt,
early_channel().0,
Arc::new(IngressMetrics::default()),
claim,
control_tx,
0,
None,
None, RingSizing::for_max_connections(4).expect("a supported cap"),
Arc::new(AtomicBool::new(false)),
)
.expect("the reader starts");
std::thread::sleep(Duration::from_millis(50));
(handle, consumer, control)
}
fn early_channel() -> (
EarlyReplySender<TestReport>,
crate::early_reply::EarlyReplyQueue<TestReport>,
) {
early_channel_of(crate::early_reply::EARLY_REPLY_QUEUE_CAPACITY)
}
fn early_channel_of(
capacity: usize,
) -> (
EarlyReplySender<TestReport>,
crate::early_reply::EarlyReplyQueue<TestReport>,
) {
crate::early_reply::early_reply_channel(capacity)
}
fn run_process_frames_with(
conn: &mut ConnectionEntry<UnixStream>,
producer: &mut ring::Producer<InputSlot<TestEvent>>,
halt: &HaltGate,
early: &mut EarlyReplySender<TestReport>,
) -> FrameAction {
run_process_frames_counting(conn, producer, halt, early, &IngressMetrics::default())
}
fn run_process_frames_counting(
conn: &mut ConnectionEntry<UnixStream>,
producer: &mut ring::Producer<InputSlot<TestEvent>>,
halt: &HaltGate,
early: &mut EarlyReplySender<TestReport>,
metrics: &IngressMetrics,
) -> FrameAction {
#[cfg(feature = "latency-trace")]
let mut publish_rec = melin_transport_core::trace::register_stage("test: publish");
#[cfg(feature = "tick-to-trade")]
let mut ingest_rec = melin_transport_core::trace::register_stage("test: ingest");
#[allow(clippy::let_unit_value)] let recv_ts = melin_transport_core::trace::mono_trace_ns();
process_frames::<TestApp, UnixStream>(
conn,
producer,
&ByteDecoder,
halt,
early,
metrics,
0xDEAD_BEEF,
recv_ts,
#[cfg(feature = "latency-trace")]
&mut publish_rec,
#[cfg(feature = "tick-to-trade")]
&mut ingest_rec,
)
}
fn ingress_counts_of(answers: &[EarlyReply<TestReport>]) -> IngressTally {
let mut tally = IngressTally::default();
for reply in answers {
match reply.kind {
EarlyKind::Busy => tally.shed += 1,
EarlyKind::Filtered => tally.filtered += 1,
EarlyKind::Refused { kind, .. } => tally.refused(kind),
EarlyKind::Rejected(_) => {}
}
}
tally
}
fn drain(
consumer: &mut ring::Consumer<InputSlot<TestEvent>>,
) -> Vec<(u64, InputSlot<TestEvent>)> {
let mut out = Vec::new();
while let Some(pair) = consumer.try_consume() {
out.push(pair);
}
out
}
fn peer_got_bytes(peer: &mut UnixStream) -> bool {
let mut buf = [0u8; 1];
match peer.read(&mut buf) {
Ok(n) => n > 0,
Err(e) if e.kind() == ErrorKind::WouldBlock || e.kind() == ErrorKind::TimedOut => false,
Err(e) => panic!("unexpected peer read error: {e}"),
}
}
#[test]
fn process_frames_publishes_all_frames_after_single_commit() {
let Fixture {
mut conn,
mut producer,
mut consumer,
mut peer,
} = make_fixture(16);
for byte in [0x01, 0x02, 0x03, 0x04, 0x05] {
conn.ingress.extend_from_slice(&frame(byte));
}
let (action, early) = run_process_frames(&mut conn, &mut producer);
assert_eq!(
action,
FrameAction::Continue,
"no oversize frame ⇒ no disconnect"
);
let events = drain(&mut consumer);
assert_eq!(events.len(), 5, "all 5 frames must be visible");
for (i, (seq, slot)) in events.iter().enumerate() {
assert_eq!(*seq, i as u64, "seq monotonic from 0");
assert_eq!(slot.connection_id, 7);
assert_eq!(slot.key_hash, 0xC0FFEE_u64);
let byte = (i + 1) as u8;
assert_eq!(slot.request_id, id_of(byte));
assert_eq!(slot.event, JournalEvent::App(TestEvent::Cmd(byte)));
assert_eq!(slot.timestamp_ns, 0xDEAD_BEEF);
}
assert!(conn.ingress.bytes().is_empty());
assert!(early.is_empty(), "{early:?}");
assert!(
!peer_got_bytes(&mut peer),
"reader must never write the client socket"
);
}
#[test]
fn process_frames_rebuilds_every_field_of_a_reused_slot() {
const CAPACITY: usize = 4;
let Fixture {
mut conn,
mut producer,
mut consumer,
..
} = make_fixture(CAPACITY);
let stale = InputSlot {
connection_id: 99,
request_id: RequestId(0xBAD),
key_hash: 1,
sequence: 77,
timestamp_ns: 5,
event: JournalEvent::App(TestEvent::Cmd(0xEE)),
..InputSlot::default()
};
for _ in 0..CAPACITY {
producer.publish(stale);
}
assert_eq!(drain(&mut consumer).len(), CAPACITY, "every slot dirtied");
for byte in 1..=CAPACITY as u8 {
conn.ingress.extend_from_slice(&frame(byte));
}
let (action, _early) = run_process_frames(&mut conn, &mut producer);
assert_eq!(action, FrameAction::Continue);
let events = drain(&mut consumer);
assert_eq!(events.len(), CAPACITY, "every dirtied slot reused");
for (i, (_, slot)) in events.iter().enumerate() {
assert_eq!(slot.connection_id, 7);
assert_eq!(slot.request_id, id_of(i as u8 + 1), "the frame's own ID");
assert_eq!(slot.key_hash, 0xC0FFEE_u64);
assert_eq!(slot.sequence, 0, "the journal stage allocates it");
assert_eq!(slot.timestamp_ns, 0xDEAD_BEEF);
assert_eq!(slot.event, JournalEvent::App(TestEvent::Cmd(i as u8 + 1)));
}
}
#[test]
fn a_halted_reader_refuses_writes_before_publishing_and_still_publishes_queries() {
let Fixture {
mut conn,
mut producer,
mut consumer,
..
} = make_fixture(16);
for byte in [0x01, 0xFF, 0x02, 0xFF] {
conn.ingress.extend_from_slice(&frame(byte));
}
let (mut early, mut queue) = early_channel();
let counted = Arc::new(AtomicU64::new(0));
let action = run_process_frames_with(
&mut conn,
&mut producer,
&gate_counting_into(Some(0), false, Arc::clone(&counted)),
&mut early,
);
assert_eq!(action, FrameAction::Continue);
assert!(conn.ingress.bytes().is_empty(), "every frame consumed");
assert_eq!(
refused_count(&counted),
2,
"the two writes are counted, the queries are not"
);
let published: Vec<_> = drain(&mut consumer)
.into_iter()
.map(|(seq, slot)| (seq, slot.event))
.collect();
assert_eq!(
published,
[
(0, JournalEvent::App(TestEvent::Query)),
(1, JournalEvent::App(TestEvent::Query)),
],
"only the queries enter the pipeline"
);
let expect = |byte| {
early_reply(
id_of(byte),
EarlyKind::Rejected((TestEvent::Cmd(byte), RejectReason::ReplicaDisconnected)),
)
};
assert_eq!(
released(&mut queue),
[expect(0x01), expect(0x02)],
"each write is answered under its ID"
);
}
#[test]
fn a_fenced_reader_closes_the_connection() {
let Fixture {
mut conn,
mut producer,
mut consumer,
..
} = make_fixture(16);
for byte in [0x01, 0xFF] {
conn.ingress.extend_from_slice(&frame(byte));
}
let (mut early, mut queue) = early_channel();
let counted = Arc::new(AtomicU64::new(0));
let action = run_process_frames_with(
&mut conn,
&mut producer,
&gate_counting_into(Some(1), true, Arc::clone(&counted)),
&mut early,
);
assert_eq!(action, FrameAction::Disconnect, "the connection is dropped");
assert!(
drain(&mut consumer).is_empty(),
"not even the query is published"
);
assert!(released(&mut queue).is_empty(), "no early reply is queued");
assert_eq!(refused_count(&counted), 0);
}
#[test]
fn a_full_early_reply_queue_parks_before_the_frame() {
let refused_as =
|byte: u8, kind, reason| early_reply(id_of(byte), EarlyKind::Refused { kind, reason });
let rejected = |byte: u8| {
early_reply(
id_of(byte),
EarlyKind::Rejected((TestEvent::Cmd(byte), RejectReason::ReplicaDisconnected)),
)
};
let cmd_3 = [JournalEvent::App(TestEvent::Cmd(0x03))];
type Case<'a> = (
usize,
bool,
u8,
Vec<EarlyReply<TestReport>>,
&'a [JournalEvent<TestEvent>],
);
let cases: [Case; 5] = [
(0, true, 0x02, vec![rejected(0x02), rejected(0x03)], &[]),
(
0,
false,
0xFC,
vec![early_reply(id_of(0xFC), EarlyKind::Filtered)],
&cmd_3,
),
(
0,
false,
0xFD,
vec![refused_as(0xFD, RefusalKind::PermissionDenied, "denied")],
&cmd_3,
),
(
0,
false,
0xFE,
vec![refused_as(0xFE, RefusalKind::Malformed, "bad")],
&cmd_3,
),
(
1,
false,
0x02,
vec![
early_reply(id_of(0x02), EarlyKind::Busy),
early_reply(id_of(0x03), EarlyKind::Busy),
],
&[],
),
];
for (prefill, halted, byte, answers, publishes) in cases {
let Fixture {
mut conn,
mut producer,
mut consumer,
..
} = make_fixture(2);
for _ in 0..prefill {
producer.publish(InputSlot::default());
}
conn.ingress.extend_from_slice(&frame(0xFF));
conn.ingress.extend_from_slice(&frame(byte));
conn.ingress.extend_from_slice(&frame(0x03));
let (mut early, mut queue) = early_channel_of(4);
let filler = early_reply(RequestId(0), EarlyKind::Busy);
let mut filled = 0;
while early.try_send(filler).is_ok() {
filled += 1;
}
let counted = Arc::new(AtomicU64::new(0));
let halt = gate_counting_into(halted.then_some(0), false, Arc::clone(&counted));
let metrics = IngressMetrics::default();
let action =
run_process_frames_counting(&mut conn, &mut producer, &halt, &mut early, &metrics);
assert_eq!(action, FrameAction::Park, "byte {byte:#04x}");
assert_eq!(
refused_count(&counted),
0,
"byte {byte:#04x}: a write parked on is not refused yet"
);
assert_eq!(
metrics.counts(),
IngressTally::default(),
"byte {byte:#04x}: nor is anything else counted"
);
let published: Vec<_> = drain(&mut consumer)
.into_iter()
.skip(prefill)
.map(|(_, slot)| slot.event)
.collect();
assert_eq!(
published,
[JournalEvent::App(TestEvent::Query)],
"byte {byte:#04x}: the frame before is committed, nothing after"
);
assert_eq!(
conn.ingress.bytes(),
[frame(byte), frame(0x03)].concat(),
"byte {byte:#04x}: the frame parked on stays, with the tail"
);
let queued = released(&mut queue);
assert_eq!(
queued.len(),
filled,
"byte {byte:#04x}: nothing more was queued"
);
assert!(queued.iter().all(|reply| *reply == filler));
let action =
run_process_frames_counting(&mut conn, &mut producer, &halt, &mut early, &metrics);
assert_eq!(action, FrameAction::Continue, "byte {byte:#04x}");
assert_eq!(
metrics.counts(),
ingress_counts_of(&answers),
"byte {byte:#04x}: each answer counted once, as its kind"
);
assert_eq!(released(&mut queue), answers, "byte {byte:#04x}");
let published: Vec<_> = drain(&mut consumer)
.into_iter()
.map(|(_, slot)| slot.event)
.collect();
assert_eq!(published, publishes, "byte {byte:#04x}");
assert!(conn.ingress.bytes().is_empty(), "byte {byte:#04x}");
assert_eq!(conn.ingress.shed_remaining(), 0, "byte {byte:#04x}");
assert_eq!(
refused_count(&counted),
if halted { 2 } else { 0 },
"byte {byte:#04x}: each refused write counted once"
);
}
}
#[test]
fn every_way_out_leaves_the_early_replies_it_staged_visible() {
let refused = |byte: u8| {
early_reply(
id_of(byte),
EarlyKind::Refused {
kind: if byte == 0xFD {
RefusalKind::PermissionDenied
} else {
RefusalKind::Malformed
},
reason: if byte == 0xFD { "denied" } else { "bad" },
},
)
};
let busy = |byte: u8| early_reply(id_of(byte), EarlyKind::Busy);
let not_a_request = tagged_frame(0x00, &[0x03; 9]);
let cases = [
(
"every frame processed",
16,
16,
vec![frame(0xFC), frame(0x01)],
FrameAction::Continue,
vec![early_reply(id_of(0xFC), EarlyKind::Filtered)],
),
(
"a full ring",
2,
16,
vec![
frame(0xFD),
frame(0x01),
frame(0x02),
frame(0x03),
frame(0x04),
],
FrameAction::Continue,
vec![refused(0xFD), busy(0x03), busy(0x04)],
),
(
"a frame that is not a request",
16,
16,
vec![frame(0xFE), not_a_request.clone()],
FrameAction::Disconnect,
vec![refused(0xFE)],
),
(
"a frame that is not a request, in a shed tail",
2,
16,
vec![
frame(0x01),
frame(0x02),
frame(0x03),
not_a_request,
frame(0x04),
],
FrameAction::Disconnect,
vec![busy(0x03)],
),
(
"an oversized prefix",
16,
16,
vec![frame(0xFC), oversize_prefix().to_vec()],
FrameAction::Disconnect,
vec![early_reply(id_of(0xFC), EarlyKind::Filtered)],
),
(
"a park",
16,
2,
vec![frame(0xFC), frame(0xFE), frame(0xFD)],
FrameAction::Park,
vec![early_reply(id_of(0xFC), EarlyKind::Filtered), refused(0xFE)],
),
(
"a park in a shed tail",
2,
2,
vec![
frame(0x01),
frame(0x02),
frame(0x03),
frame(0x04),
frame(0x05),
],
FrameAction::Park,
vec![busy(0x03), busy(0x04)],
),
];
for (case, ring_capacity, queue_capacity, frames, outcome, expected) in cases {
let Fixture {
mut conn,
mut producer,
..
} = make_fixture(ring_capacity);
for frame in frames {
conn.ingress.extend_from_slice(&frame);
}
let (mut early, mut queue) = early_channel_of(queue_capacity);
let action =
run_process_frames_with(&mut conn, &mut producer, &gate(None, false), &mut early);
assert_eq!(action, outcome, "{case}");
assert_eq!(released(&mut queue), expected, "{case}");
}
}
#[cfg(feature = "latency-trace")]
#[test]
fn process_frames_stamps_every_slot_with_caller_recv_ts() {
let Fixture {
mut conn,
mut producer,
mut consumer,
..
} = make_fixture(16);
for byte in [0x01, 0x02, 0x03, 0x04] {
conn.ingress.extend_from_slice(&frame(byte));
}
const RECV_TS: u64 = 0x5EED_5EED;
let mut publish_rec = melin_transport_core::trace::register_stage("test: publish recv_ts");
#[cfg(feature = "tick-to-trade")]
let mut ingest_rec = melin_transport_core::trace::register_stage("test: ingest recv_ts");
let (mut early, _queue) = early_channel();
let action = process_frames::<TestApp, UnixStream>(
&mut conn,
&mut producer,
&ByteDecoder,
&gate(None, false),
&mut early,
&IngressMetrics::default(),
0xDEAD_BEEF,
RECV_TS,
&mut publish_rec,
#[cfg(feature = "tick-to-trade")]
&mut ingest_rec,
);
assert_eq!(action, FrameAction::Continue);
let events = drain(&mut consumer);
assert_eq!(events.len(), 4, "all frames published");
for (_seq, slot) in &events {
assert_eq!(
slot.recv_ts, RECV_TS,
"every frame from one recv shares the single caller-supplied recv_ts"
);
}
}
#[test]
fn process_frames_rotates_batch_at_commit_every_cap() {
let Fixture {
mut conn,
mut producer,
mut consumer,
..
} = make_fixture(64);
const EVENT_COUNT: usize = 32;
for i in 0..EVENT_COUNT {
conn.ingress.extend_from_slice(&frame((i + 1) as u8));
}
let (action, _early) = run_process_frames(&mut conn, &mut producer);
assert_eq!(
action,
FrameAction::Continue,
"no oversize / no Full ⇒ no disconnect"
);
let events = drain(&mut consumer);
assert_eq!(events.len(), EVENT_COUNT, "every event visible");
for (i, (seq, slot)) in events.iter().enumerate() {
assert_eq!(*seq, i as u64, "seq contiguous across batch rotations");
let byte = (i + 1) as u8;
assert_eq!(slot.event, JournalEvent::App(TestEvent::Cmd(byte)));
}
assert!(conn.ingress.bytes().is_empty());
}
#[test]
fn process_frames_query_event_skips_wall_clock_stamp() {
let Fixture {
mut conn,
mut producer,
mut consumer,
..
} = make_fixture(8);
conn.ingress.extend_from_slice(&frame(0xFF));
let (action, _early) = run_process_frames(&mut conn, &mut producer);
assert_eq!(action, FrameAction::Continue);
let events = drain(&mut consumer);
assert_eq!(events.len(), 1);
let (_, slot) = &events[0];
assert_eq!(slot.event, JournalEvent::App(TestEvent::Query));
assert_eq!(
slot.timestamp_ns, 0,
"query events must skip the wall-clock stamp"
);
}
#[test]
fn a_full_ring_sheds_the_frame_that_met_it_and_every_complete_frame_behind() {
let Fixture {
mut conn,
mut producer,
mut consumer,
mut peer,
} = make_fixture(4);
for byte in [0x01, 0x02, 0x03, 0x04, 0x05, 0x06] {
conn.ingress.extend_from_slice(&frame(byte));
}
let partial = frame(0x07);
conn.ingress.extend_from_slice(&partial[..5]);
let (action, early) = run_process_frames(&mut conn, &mut producer);
assert_eq!(
action,
FrameAction::Continue,
"Full does not drop the connection"
);
let events = drain(&mut consumer);
assert_eq!(
events.len(),
4,
"only the first capacity-many frames are visible"
);
for (i, (_, slot)) in events.iter().enumerate() {
let byte = (i + 1) as u8;
assert_eq!(slot.event, JournalEvent::App(TestEvent::Cmd(byte)));
}
assert_eq!(
early,
[
early_reply(id_of(0x05), EarlyKind::Busy),
early_reply(id_of(0x06), EarlyKind::Busy),
],
"a busy for the frame that met the full ring, and for the one behind"
);
assert!(
!peer_got_bytes(&mut peer),
"reader must never write the client socket"
);
assert_eq!(conn.ingress.bytes(), &partial[..5]);
assert_eq!(conn.ingress.shed_remaining(), 0);
conn.ingress.extend_from_slice(&partial[5..]);
let (action, early) = run_process_frames(&mut conn, &mut producer);
assert_eq!(action, FrameAction::Continue);
assert!(early.is_empty(), "{early:?}");
let events = drain(&mut consumer);
assert_eq!(events.len(), 1);
assert_eq!(events[0].1.event, JournalEvent::App(TestEvent::Cmd(0x07)));
}
#[test]
fn pipeline_full_never_touches_the_client_socket() {
let Fixture {
mut conn,
mut producer,
mut consumer,
mut peer,
} = make_fixture(2);
let junk = [0u8; 4096];
loop {
let n = unsafe {
libc::send(
conn.fd,
junk.as_ptr().cast(),
junk.len(),
libc::MSG_DONTWAIT,
)
};
if n < 0 {
assert_eq!(
std::io::Error::last_os_error().raw_os_error(),
Some(libc::EAGAIN),
"unexpected errno while filling socket"
);
break;
}
}
conn.ingress.extend_from_slice(&frame(0x01));
conn.ingress.extend_from_slice(&frame(0x02));
conn.ingress.extend_from_slice(&frame(0x03));
let start = std::time::Instant::now();
let (action, early) = run_process_frames(&mut conn, &mut producer);
assert!(
start.elapsed() < Duration::from_secs(1),
"pipeline-full path blocked on a full socket"
);
assert_eq!(
action,
FrameAction::Continue,
"Full does not drop the connection"
);
assert_eq!(
drain(&mut consumer).len(),
2,
"the frames that fit are still published"
);
assert_eq!(
early,
[early_reply(id_of(0x03), EarlyKind::Busy)],
"busy notice routed to response stage"
);
let mut sink = [0u8; 4096];
loop {
match peer.read(&mut sink) {
Ok(0) => panic!("unexpected EOF"),
Ok(_) => continue,
Err(e) if e.kind() == ErrorKind::WouldBlock || e.kind() == ErrorKind::TimedOut => {
break;
}
Err(e) => panic!("unexpected peer read error: {e}"),
}
}
}
#[test]
fn process_frames_oversize_commits_prior_frames_then_signals_disconnect() {
let Fixture {
mut conn,
mut producer,
mut consumer,
mut peer,
} = make_fixture(16);
conn.ingress.extend_from_slice(&frame(0x01));
conn.ingress.extend_from_slice(&frame(0x02));
conn.ingress.extend_from_slice(&oversize_prefix());
let (action, early) = run_process_frames(&mut conn, &mut producer);
assert_eq!(
action,
FrameAction::Disconnect,
"oversize frame must request disconnect"
);
let events = drain(&mut consumer);
assert_eq!(
events.len(),
2,
"prior frames are committed before the break"
);
assert_eq!(events[0].1.event, JournalEvent::App(TestEvent::Cmd(0x01)));
assert_eq!(events[1].1.event, JournalEvent::App(TestEvent::Cmd(0x02)));
assert!(early.is_empty(), "{early:?}");
assert!(
!peer_got_bytes(&mut peer),
"reader must never write the client socket"
);
}
#[test]
fn each_decoder_outcome_yields_its_early_reply_with_the_frames_id() {
let Fixture {
mut conn,
mut producer,
mut consumer,
..
} = make_fixture(16);
for byte in [0x01, 0xFC, 0xFD, 0xFE, 0x02] {
conn.ingress.extend_from_slice(&frame(byte));
}
conn.ingress.extend_from_slice(&request_frame(99, b""));
let (action, early) = run_process_frames(&mut conn, &mut producer);
assert_eq!(
action,
FrameAction::Continue,
"a refused request keeps the connection"
);
let events = drain(&mut consumer);
assert_eq!(events.len(), 2);
assert_eq!(events[0].1.event, JournalEvent::App(TestEvent::Cmd(0x01)));
assert_eq!(events[1].1.event, JournalEvent::App(TestEvent::Cmd(0x02)));
assert_eq!(
early,
[
early_reply(id_of(0xFC), EarlyKind::Filtered),
early_reply(
id_of(0xFD),
EarlyKind::Refused {
kind: RefusalKind::PermissionDenied,
reason: "denied"
}
),
early_reply(
id_of(0xFE),
EarlyKind::Refused {
kind: RefusalKind::Malformed,
reason: "bad"
}
),
early_reply(
RequestId(99),
EarlyKind::Refused {
kind: RefusalKind::Malformed,
reason: "empty body"
}
),
]
);
assert!(
conn.ingress.bytes().is_empty(),
"all bytes advanced past compaction"
);
}
#[test]
fn a_frame_that_is_not_a_request_closes_the_connection() {
let mut not_requests = vec![
0u32.to_le_bytes().to_vec(),
tagged_frame(melin_wire_protocol::control_codec::TAG_REQUEST, &[0x03; 7]),
tagged_frame(melin_wire_protocol::control_codec::TAG_REQUEST, &[]),
];
for tag in [
0x00,
TAG_CHALLENGE_RESPONSE,
TAG_RESPONSE_HEARTBEAT,
TAG_APP,
0x10,
] {
not_requests.push(tagged_frame(tag, &[0x03; 9]));
}
for bad in not_requests {
let Fixture {
mut conn,
mut producer,
mut consumer,
..
} = make_fixture(16);
conn.ingress.extend_from_slice(&frame(0x01));
conn.ingress.extend_from_slice(&frame(0xFC));
conn.ingress.extend_from_slice(&bad);
conn.ingress.extend_from_slice(&frame(0x02));
let (action, early) = run_process_frames(&mut conn, &mut producer);
assert_eq!(
action,
FrameAction::Disconnect,
"{bad:?} must close the connection"
);
let events = drain(&mut consumer);
assert_eq!(events.len(), 1, "{bad:?}");
assert_eq!(events[0].1.event, JournalEvent::App(TestEvent::Cmd(0x01)));
assert_eq!(early, [early_reply(id_of(0xFC), EarlyKind::Filtered)]);
}
}
#[test]
fn process_frames_preserves_partial_trailing_frame() {
let Fixture {
mut conn,
mut producer,
mut consumer,
..
} = make_fixture(16);
conn.ingress.extend_from_slice(&frame(0x42));
conn.ingress.extend_from_slice(&[0xDE, 0xAD, 0xBE]);
let (action, _early) = run_process_frames(&mut conn, &mut producer);
assert_eq!(action, FrameAction::Continue);
let events = drain(&mut consumer);
assert_eq!(events.len(), 1);
assert_eq!(events[0].1.event, JournalEvent::App(TestEvent::Cmd(0x42)));
assert_eq!(
conn.ingress.bytes(),
[0xDE, 0xAD, 0xBE],
"partial length prefix preserved for next recv-cycle"
);
}
#[test]
fn process_frames_admits_a_frame_at_the_limit_and_drops_one_past_it() {
let Fixture {
mut conn,
mut producer,
mut consumer,
..
} = make_fixture(16);
let mut body = vec![0x05; MAX_REQUEST_BODY];
body[0] = 0x2A;
let at_limit = request_frame(5, &body);
assert_eq!(at_limit.len(), 4 + MAX_FRAME_SIZE);
conn.ingress.extend_from_slice(&at_limit[..4 + 1]);
let (action, _early) = run_process_frames(&mut conn, &mut producer);
assert_eq!(
action,
FrameAction::Continue,
"a frame at the limit is not oversized"
);
assert_eq!(conn.ingress.bytes(), &at_limit[..4 + 1]);
assert!(drain(&mut consumer).is_empty());
conn.ingress.extend_from_slice(&at_limit[4 + 1..]);
let (action, _early) = run_process_frames(&mut conn, &mut producer);
assert_eq!(action, FrameAction::Continue);
let events = drain(&mut consumer);
assert_eq!(events.len(), 1);
assert_eq!(events[0].1.event, JournalEvent::App(TestEvent::Cmd(0x2A)));
assert_eq!(events[0].1.request_id, RequestId(5));
assert!(conn.ingress.bytes().is_empty());
conn.ingress.extend_from_slice(&oversize_prefix());
let (action, _early) = run_process_frames(&mut conn, &mut producer);
assert_eq!(
action,
FrameAction::Disconnect,
"oversize prefix alone must request disconnect"
);
assert!(drain(&mut consumer).is_empty());
}
#[test]
fn process_frames_empty_buffer_is_noop() {
let Fixture {
mut conn,
mut producer,
mut consumer,
mut peer,
} = make_fixture(4);
let (action, early) = run_process_frames(&mut conn, &mut producer);
assert_eq!(action, FrameAction::Continue);
assert_eq!(drain(&mut consumer).len(), 0);
assert!(conn.ingress.bytes().is_empty());
assert!(early.is_empty());
assert!(!peer_got_bytes(&mut peer));
}
mod shedding {
use super::*;
use proptest::prelude::*;
fn body() -> impl Strategy<Value = u8> {
prop_oneof![1u8..=8, Just(0xFC), Just(0xFD), Just(0xFE), Just(0xFF)]
}
prop_compose! {
fn script()(
bodies in prop::collection::vec(body(), 1..40),
ring_capacity in prop_oneof![Just(2usize), Just(4), Just(8)],
queue_capacity in prop_oneof![Just(2usize), Just(4)],
)(
cuts in prop::collection::vec(0..bodies.len() * 14 + 1, 0..12),
drains in prop::collection::vec((0usize..4, any::<bool>()), 64),
bodies in Just(bodies),
ring_capacity in Just(ring_capacity),
queue_capacity in Just(queue_capacity),
) -> (Vec<u8>, Vec<usize>, Vec<(usize, bool)>, usize, usize) {
(bodies, cuts, drains, ring_capacity, queue_capacity)
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(256))]
#[test]
fn every_request_is_published_or_answered_once_and_a_shed_tail_whole(
(bodies, mut cuts, drains, ring_capacity, queue_capacity) in script()
) {
let frames: Vec<Vec<u8>> = bodies
.iter()
.enumerate()
.map(|(i, body)| request_frame(i as u64, &[*body]))
.collect();
let stream = frames.concat();
cuts.push(0);
cuts.push(stream.len());
cuts.sort_unstable();
cuts.dedup();
let Fixture { mut conn, mut producer, mut consumer, .. } =
make_fixture(ring_capacity);
let (mut early, mut queue) = early_channel_of(queue_capacity);
let halt = gate(None, false);
let metrics = IngressMetrics::default();
let mut published: Vec<u64> = Vec::new();
let mut answered: Vec<EarlyReply<TestReport>> = Vec::new();
let mut episodes: Vec<usize> = Vec::new();
let mut drains = drains.into_iter().cycle();
let mut take = |consumer: &mut ring::Consumer<InputSlot<TestEvent>>,
queue: &mut crate::early_reply::EarlyReplyQueue<TestReport>,
published: &mut Vec<u64>,
answered: &mut Vec<EarlyReply<TestReport>>,
all: bool| {
let (events, replies) = drains.next().expect("cycled");
for _ in 0..if all { usize::MAX } else { events } {
let Some((_, slot)) = consumer.try_consume() else { break };
published.push(slot.request_id.0);
}
if all || replies {
answered.extend(released(queue));
}
};
let mut receives = cuts.windows(2).map(|w| &stream[w[0]..w[1]]);
let mut action = FrameAction::Continue;
loop {
if action == FrameAction::Park {
answered.extend(released(&mut queue));
} else if let Some(bytes) = receives.next() {
conn.ingress.extend_from_slice(bytes);
let received = stream.len() - receives.clone().map(<[u8]>::len).sum::<usize>();
episodes.push(received / frames[0].len());
} else {
break;
}
action = run_process_frames_counting(
&mut conn, &mut producer, &halt, &mut early, &metrics,
);
prop_assert_ne!(action, FrameAction::Disconnect);
take(&mut consumer, &mut queue, &mut published, &mut answered, false);
}
take(&mut consumer, &mut queue, &mut published, &mut answered, true);
prop_assert!(conn.ingress.bytes().is_empty());
prop_assert_eq!(conn.ingress.shed_remaining(), 0);
prop_assert_eq!(metrics.counts(), ingress_counts_of(&answered));
let mut seen: Vec<u64> = published.clone();
seen.extend(answered.iter().map(|reply| reply.request_id.0));
seen.sort_unstable();
prop_assert_eq!(seen, (0..bodies.len() as u64).collect::<Vec<_>>());
for id in &published {
let body = bodies[*id as usize];
prop_assert!(body <= 8 || body == 0xFF, "published {body:#04x}");
}
let mut busy: Vec<u64> = Vec::new();
for reply in &answered {
let body = bodies[reply.request_id.0 as usize];
match reply.kind {
EarlyKind::Busy => busy.push(reply.request_id.0),
EarlyKind::Filtered => prop_assert_eq!(body, 0xFC),
EarlyKind::Refused { kind: RefusalKind::PermissionDenied, .. } => {
prop_assert_eq!(body, 0xFD)
}
EarlyKind::Refused { kind: RefusalKind::Malformed, .. } => {
prop_assert_eq!(body, 0xFE)
}
EarlyKind::Rejected(_) => prop_assert!(false, "not halted"),
}
}
for &k in &busy {
let complete = episodes
.iter()
.copied()
.find(|&complete| complete > k as usize)
.expect("answered once complete");
for j in k + 1..complete as u64 {
prop_assert!(
busy.contains(&j),
"request {j} was in the tail shed at {k}, \
{complete} complete: {busy:?}"
);
}
}
}
}
}
mod recv_state {
use super::super::{
CqeStep, FrameAction, FramesStep, RecvCompletion, RecvState, TeardownStep,
};
use std::time::{Duration, Instant};
const DATA: RecvCompletion = RecvCompletion::Data;
const EMPTY: RecvCompletion = RecvCompletion::Empty;
const CLOSED: RecvCompletion = RecvCompletion::Closed;
fn parked(cancelling: bool, since: Instant) -> RecvState {
RecvState::Parked { cancelling, since }
}
#[test]
fn an_armed_receive_is_processed_and_re_armed_when_it_ends() {
let now = Instant::now();
assert_eq!(
RecvState::Armed.on_cqe(DATA, false),
(RecvState::Armed, CqeStep::Process)
);
assert_eq!(
RecvState::Armed.on_cqe(DATA, true),
(RecvState::Idle, CqeStep::Process)
);
assert_eq!(
RecvState::Idle.after_frames(FrameAction::Continue, now),
(RecvState::Armed, FramesStep::Arm)
);
assert_eq!(
RecvState::Armed.after_frames(FrameAction::Continue, now),
(RecvState::Armed, FramesStep::Nothing)
);
assert_eq!(
RecvState::Armed.on_cqe(EMPTY, true),
(RecvState::Armed, CqeStep::Arm)
);
assert_eq!(
RecvState::Armed.on_cqe(EMPTY, false),
(RecvState::Armed, CqeStep::Nothing)
);
assert_eq!(
RecvState::Armed.on_cqe(CLOSED, true),
(RecvState::Idle, CqeStep::Remove)
);
assert_eq!(RecvState::Idle.after_terminal(), RecvState::Idle);
assert_eq!(RecvState::Dying.after_terminal(), RecvState::Dying);
assert_eq!(
RecvState::Idle.on_cqe(EMPTY, true),
(RecvState::Armed, CqeStep::Arm)
);
}
#[test]
fn a_park_cancels_keeps_late_bytes_and_re_arms_on_resume() {
let since = Instant::now();
let later = since + std::time::Duration::from_secs(5);
assert_eq!(
RecvState::Armed.after_frames(FrameAction::Park, since),
(parked(true, since), FramesStep::Cancel)
);
assert_eq!(
parked(true, since).on_cqe(DATA, false),
(parked(true, since), CqeStep::Keep)
);
assert_eq!(
parked(true, since).on_cqe(DATA, true),
(parked(false, since), CqeStep::Keep)
);
assert_eq!(
parked(true, since).on_cqe(EMPTY, true),
(parked(false, since), CqeStep::Nothing),
"no re-arm while parked"
);
assert_eq!(
parked(true, since).on_cqe(EMPTY, false),
(parked(true, since), CqeStep::Nothing)
);
assert_eq!(
parked(false, since).after_frames(FrameAction::Park, later),
(parked(false, since), FramesStep::Nothing)
);
assert_eq!(
parked(false, since).after_frames(FrameAction::Continue, later),
(RecvState::Armed, FramesStep::Arm)
);
assert_eq!(
RecvState::Idle.after_frames(FrameAction::Park, since),
(parked(false, since), FramesStep::Nothing)
);
}
#[test]
fn a_resume_before_the_cancel_lands_arms_on_its_terminal_cqe() {
let since = Instant::now();
assert_eq!(
parked(true, since).after_frames(FrameAction::Continue, since),
(RecvState::Resuming, FramesStep::Nothing)
);
assert_eq!(
RecvState::Resuming.on_cqe(DATA, false),
(RecvState::Resuming, CqeStep::Process)
);
assert_eq!(
RecvState::Resuming.after_frames(FrameAction::Continue, since),
(RecvState::Resuming, FramesStep::Nothing)
);
assert_eq!(
RecvState::Resuming.on_cqe(EMPTY, true),
(RecvState::Armed, CqeStep::Arm)
);
assert_eq!(
RecvState::Resuming.on_cqe(DATA, true),
(RecvState::Idle, CqeStep::Process)
);
assert_eq!(
RecvState::Resuming.after_frames(FrameAction::Park, since),
(parked(true, since), FramesStep::Nothing)
);
}
#[test]
fn teardown_cancels_once_and_frees_once() {
let since = Instant::now();
assert_eq!(
RecvState::Armed.on_teardown(),
(RecvState::Dying, TeardownStep::Cancel)
);
for cancelling in [parked(true, since), RecvState::Resuming] {
assert_eq!(
cancelling.on_teardown(),
(RecvState::Dying, TeardownStep::AwaitCancel),
"{cancelling:?}"
);
}
for owed_nothing in [parked(false, since), RecvState::Idle] {
assert_eq!(
owed_nothing.on_teardown(),
(owed_nothing, TeardownStep::FreeNow),
"{owed_nothing:?}"
);
}
assert_eq!(
RecvState::Dying.on_teardown(),
(RecvState::Dying, TeardownStep::AlreadyDying)
);
for completion in [DATA, EMPTY, CLOSED] {
assert_eq!(
RecvState::Dying.on_cqe(completion, false),
(RecvState::Dying, CqeStep::Discard { free: false })
);
assert_eq!(
RecvState::Dying.on_cqe(completion, true),
(RecvState::Dying, CqeStep::Discard { free: true })
);
}
assert_eq!(parked(true, since).on_cqe(CLOSED, true).1, CqeStep::Remove);
for state in [
RecvState::Armed,
RecvState::Idle,
RecvState::Resuming,
parked(true, since),
] {
assert_eq!(
state.after_frames(FrameAction::Disconnect, since),
(state, FramesStep::Teardown)
);
}
}
#[test]
fn only_a_park_cancel_in_flight_past_the_deadline_is_overdue() {
let since = Instant::now();
let past = since + super::super::PARK_CANCEL_DEADLINE + Duration::from_nanos(1);
let at = since + super::super::PARK_CANCEL_DEADLINE;
assert!(parked(true, since).park_cancel_overdue(past));
assert!(!parked(true, since).park_cancel_overdue(at));
assert!(!parked(true, since).park_cancel_overdue(since));
for state in [
parked(false, since),
RecvState::Armed,
RecvState::Idle,
RecvState::Resuming,
RecvState::Dying,
] {
assert!(!state.park_cancel_overdue(past), "{state:?}");
}
}
#[test]
fn the_kernel_owes_a_cqe_while_a_receive_or_a_cancel_is_in_flight() {
let since = Instant::now();
for owed in [
RecvState::Armed,
RecvState::Resuming,
RecvState::Dying,
parked(true, since),
] {
assert!(owed.kernel_owes_cqe(), "{owed:?}");
}
for free in [RecvState::Idle, parked(false, since)] {
assert!(!free.kernel_owes_cqe(), "{free:?}");
}
}
}
#[test]
fn reader_loop_soak_delivers_every_frame_in_order() {
use std::io::Write as _;
const CONNS: u64 = 4;
const FRAMES: u32 = 10_000;
let (producer, mut consumers) = DisruptorBuilder::<InputSlot<TestEvent>>::new(65536)
.add_consumer()
.build(melin_pipeline::wait::WaitStrategy::SpinThenYield);
let mut consumer = consumers.pop().expect("consumer");
let (control_tx, control_rx) = mpsc::channel();
let shutdown = Arc::new(AtomicBool::new(false));
let mut handle = spawn_reader::<TestApp, UnixStream>(
producer,
Arc::new(ByteDecoder),
gate(None, false),
early_channel().0,
Arc::new(IngressMetrics::default()),
policy_claim(),
control_tx,
0, None, None, RingSizing::for_max_connections(CONNS).expect("a supported cap"),
Arc::clone(&shutdown),
)
.expect("the reader starts");
let mut writers = Vec::new();
for id in 0..CONNS {
let (client, server_side) = UnixStream::pair().expect("socketpair");
handle.register(ReaderRegistration {
connection_id: ConnectionId(id),
reader: server_side,
addr: "127.0.0.1:1".parse().expect("addr"),
role: ClientRole::Operator,
key_hash: id,
});
writers.push(std::thread::spawn(move || {
let mut client = client;
for i in 0..FRAMES {
let byte = (i % 200 + 1) as u8;
client.write_all(&frame(byte)).expect("client write");
if i % 512 == 0 {
std::thread::sleep(Duration::from_micros(200));
}
}
client }));
}
let deadline = Instant::now() + Duration::from_secs(60);
let mut per_conn_count = [0u32; CONNS as usize];
let mut total: u64 = 0;
while total < CONNS * FRAMES as u64 {
assert!(
Instant::now() < deadline,
"soak stalled: {total} of {} frames after 60s ({per_conn_count:?})",
CONNS * FRAMES as u64
);
while let Some((_seq, slot)) = consumer.try_consume() {
let conn = slot.connection_id as usize;
assert!(conn < CONNS as usize, "unknown connection id");
let expected = (per_conn_count[conn] % 200 + 1) as u8;
assert_eq!(
slot.event,
JournalEvent::App(TestEvent::Cmd(expected)),
"conn {conn} frame {} out of order or corrupted",
per_conn_count[conn]
);
per_conn_count[conn] += 1;
total += 1;
}
std::hint::spin_loop();
}
if let Ok(event) = control_rx.try_recv() {
match event {
ControlEvent::Disconnected { connection_id } => {
panic!("spurious disconnect of connection {connection_id}")
}
ControlEvent::Connected { .. } => {
unreachable!("reader never sends Connected")
}
}
}
let _clients: Vec<UnixStream> = writers
.into_iter()
.map(|w| w.join().expect("writer thread"))
.collect();
handle.shutdown();
handle.join();
}
#[test]
fn shutdown_quiesces_armed_connections_promptly() {
use std::io::Write as _;
const CONNS: u64 = 3;
let (producer, mut consumers) = DisruptorBuilder::<InputSlot<TestEvent>>::new(1024)
.add_consumer()
.build(melin_pipeline::wait::WaitStrategy::SpinThenYield);
let mut consumer = consumers.pop().expect("consumer");
let (control_tx, _control_rx) = mpsc::channel();
let shutdown = Arc::new(AtomicBool::new(false));
let mut handle = spawn_reader::<TestApp, UnixStream>(
producer,
Arc::new(ByteDecoder),
gate(None, false),
early_channel().0,
Arc::new(IngressMetrics::default()),
policy_claim(),
control_tx,
0, None, None, RingSizing::for_max_connections(CONNS).expect("a supported cap"),
Arc::clone(&shutdown),
)
.expect("the reader starts");
let mut clients = Vec::new();
for id in 0..CONNS {
let (mut client, server_side) = UnixStream::pair().expect("socketpair");
handle.register(ReaderRegistration {
connection_id: ConnectionId(id),
reader: server_side,
addr: "127.0.0.1:1".parse().expect("addr"),
role: ClientRole::Operator,
key_hash: id,
});
client.write_all(&frame(7)).expect("client write");
clients.push(client);
}
let deadline = Instant::now() + Duration::from_secs(30);
let mut seen = 0u64;
while seen < CONNS {
assert!(
Instant::now() < deadline,
"frames never arrived ({seen}/{CONNS})"
);
while consumer.try_consume().is_some() {
seen += 1;
}
std::hint::spin_loop();
}
let started = Instant::now();
handle.shutdown();
handle.join();
let elapsed = started.elapsed();
assert!(
elapsed < crate::uring_teardown::DRAIN_TIMEOUT / 4,
"reader teardown took {elapsed:?}, close to the drain deadline — \
armed operations were not woken"
);
drop(clients);
}
#[test]
fn torn_down_connection_cannot_inject_frames_into_a_reused_slot() {
use std::io::Write as _;
let (producer, mut consumers) = DisruptorBuilder::<InputSlot<TestEvent>>::new(65536)
.add_consumer()
.build(melin_pipeline::wait::WaitStrategy::SpinThenYield);
let mut consumer = consumers.pop().expect("consumer");
let (control_tx, control_rx) = mpsc::channel();
let shutdown = Arc::new(AtomicBool::new(false));
let mut handle = spawn_reader::<TestApp, UnixStream>(
producer,
Arc::new(ByteDecoder),
gate(None, false),
early_channel().0,
Arc::new(IngressMetrics::default()),
policy_claim(),
control_tx,
0,
None,
None,
RingSizing::for_max_connections(4).expect("a supported cap"),
Arc::clone(&shutdown),
)
.expect("the reader starts");
const A_ID: u64 = 100;
const B_ID: u64 = 200;
let (a_client, a_server) = UnixStream::pair().expect("socketpair");
handle.register(ReaderRegistration {
connection_id: ConnectionId(A_ID),
reader: a_server,
addr: "127.0.0.1:1".parse().expect("addr"),
role: ClientRole::Operator,
key_hash: A_ID,
});
let a_writer = std::thread::spawn(move || {
let mut a = a_client;
for byte in 0x01..=0x08u8 {
let _ = a.write_all(&frame(byte));
}
let _ = a.write_all(&oversize_prefix());
let stop = Instant::now() + Duration::from_millis(300);
while Instant::now() < stop {
let _ = a.write_all(&frame(0x05));
std::thread::sleep(Duration::from_micros(50));
}
a
});
let deadline = Instant::now() + Duration::from_secs(10);
loop {
match control_rx.try_recv() {
Ok(ControlEvent::Disconnected { connection_id }) if connection_id == A_ID => break,
Ok(_) => {}
Err(_) => {
assert!(Instant::now() < deadline, "no Disconnected for A");
std::thread::sleep(Duration::from_millis(1));
}
}
}
const B_FRAMES: u32 = 2_000;
let (b_client, b_server) = UnixStream::pair().expect("socketpair");
handle.register(ReaderRegistration {
connection_id: ConnectionId(B_ID),
reader: b_server,
addr: "127.0.0.1:2".parse().expect("addr"),
role: ClientRole::Operator,
key_hash: B_ID,
});
let b_writer = std::thread::spawn(move || {
let mut b = b_client;
for i in 0..B_FRAMES {
let byte = 0x21 + (i % 0x10) as u8; b.write_all(&frame(byte)).expect("B write");
if i % 128 == 0 {
std::thread::sleep(Duration::from_micros(100));
}
}
b
});
let deadline = Instant::now() + Duration::from_secs(30);
let mut b_seen = 0u32;
while b_seen < B_FRAMES {
assert!(
Instant::now() < deadline,
"stalled: {b_seen}/{B_FRAMES} B frames"
);
while let Some((_seq, slot)) = consumer.try_consume() {
match slot.event {
JournalEvent::App(TestEvent::Cmd(byte)) => {
if slot.connection_id == B_ID {
assert_eq!(slot.key_hash, B_ID, "B slot with foreign key hash");
assert!(
(0x21..=0x30).contains(&byte),
"byte {byte:#x} injected into B's stream"
);
b_seen += 1;
} else {
assert_eq!(slot.connection_id, A_ID, "unknown connection id");
assert!(
(0x01..=0x08).contains(&byte),
"unexpected byte {byte:#x} on A"
);
}
}
ref other => panic!("unexpected event {other:?}"),
}
}
std::hint::spin_loop();
}
let _a = a_writer.join().expect("A writer");
let _b = b_writer.join().expect("B writer");
handle.shutdown();
handle.join();
}
mod parking {
use super::*;
use crate::early_reply::EarlyReplyQueue;
use melin_transport_core::fence::FenceState;
use std::io::Write as _;
const FILTERED: u8 = 0xFC;
struct Rig {
handle: UringReaderHandle<UnixStream>,
consumer: ring::Consumer<InputSlot<TestEvent>>,
queue: EarlyReplyQueue<TestReport>,
control_rx: mpsc::Receiver<ControlEvent>,
fence: Arc<FenceState>,
metrics: Arc<IngressMetrics>,
}
impl Rig {
fn start(ring_capacity: usize, connection_timeout: Option<Duration>) -> Self {
let (producer, mut consumers) =
DisruptorBuilder::<InputSlot<TestEvent>>::new(ring_capacity)
.add_consumer()
.build(melin_pipeline::wait::WaitStrategy::SpinThenYield);
let consumer = consumers.pop().expect("consumer");
let (control_tx, control_rx) = mpsc::channel();
let fence = Arc::new(FenceState::new(0));
let halt = HaltGate::new(None, Arc::clone(&fence), Arc::new(AtomicU64::new(0)));
let (early, queue) = early_channel_of(2);
let metrics = Arc::new(IngressMetrics::default());
let handle = spawn_reader::<TestApp, UnixStream>(
producer,
Arc::new(ByteDecoder),
halt,
early,
Arc::clone(&metrics),
policy_claim(),
control_tx,
0,
connection_timeout,
None,
RingSizing::for_max_connections(4).expect("a supported cap"),
Arc::new(AtomicBool::new(false)),
)
.expect("the reader starts");
Rig {
handle,
consumer,
queue,
control_rx,
fence,
metrics,
}
}
fn wait_for_parked(&self, parked: u64) {
let deadline = Instant::now() + Duration::from_secs(10);
while self.metrics.parked() != parked {
assert!(
Instant::now() < deadline,
"the parked gauge stayed at {}, not {parked}",
self.metrics.parked()
);
std::thread::sleep(Duration::from_millis(1));
}
}
fn connect(&mut self, id: u64) -> (UnixStream, UnixStream) {
let (client, server_side) = UnixStream::pair().expect("socketpair");
let dup = server_side.try_clone().expect("dup");
self.handle.register(ReaderRegistration {
connection_id: ConnectionId(id),
reader: server_side,
addr: "127.0.0.1:1".parse().expect("addr"),
role: ClientRole::Operator,
key_hash: id,
});
(client, dup)
}
fn take_replies(&mut self) -> Vec<EarlyReply<TestReport>> {
released(&mut self.queue)
}
fn take_published(&mut self) -> Vec<RequestId> {
drain(&mut self.consumer)
.into_iter()
.map(|(_, slot)| slot.request_id)
.collect()
}
fn wait_for(
&mut self,
published: &mut Vec<RequestId>,
replies: &mut Vec<EarlyReply<TestReport>>,
done: impl Fn(&[RequestId], &[EarlyReply<TestReport>]) -> bool,
) {
let deadline = Instant::now() + Duration::from_secs(10);
loop {
published.extend(self.take_published());
replies.extend(self.take_replies());
if done(published, replies) {
return;
}
assert!(
Instant::now() < deadline,
"stalled: published {published:?}, answered {replies:?}"
);
std::thread::sleep(Duration::from_millis(1));
}
}
fn disconnects(&self) -> Vec<u64> {
self.control_rx
.try_iter()
.map(|event| match event {
ControlEvent::Disconnected { connection_id } => connection_id,
ControlEvent::Connected { .. } => {
unreachable!("the reader never sends Connected")
}
})
.collect()
}
fn stop(self) {
self.handle.shutdown();
self.handle.join();
}
}
fn burst() -> Vec<u8> {
let mut bytes = Vec::new();
for id in 1..=3 {
bytes.extend(request_frame(id, &[FILTERED]));
}
for id in 4..=8 {
bytes.extend(request_frame(id, &[0x01]));
}
bytes
}
fn filtered_ids(replies: &[EarlyReply<TestReport>]) -> Vec<RequestId> {
replies
.iter()
.map(|reply| {
assert_eq!(reply.kind, EarlyKind::Filtered, "{reply:?}");
reply.request_id
})
.collect()
}
fn park(rig: &mut Rig, client: &mut UnixStream) {
client.write_all(&burst()).expect("client write");
rig.wait_for_parked(1);
let published = rig.take_published();
assert!(published.is_empty(), "published past a park: {published:?}");
}
#[test]
fn a_parked_connection_resumes_without_new_bytes() {
let mut rig = Rig::start(64, None);
let (mut client, _dup) = rig.connect(1);
park(&mut rig, &mut client);
for id in 9..=12 {
client
.write_all(&request_frame(id, &[0x02]))
.expect("client write");
}
std::thread::sleep(Duration::from_millis(50));
assert!(
rig.take_published().is_empty(),
"nothing read past the park"
);
assert_eq!(rig.metrics.parked(), 1);
assert_eq!(rig.metrics.parks(), 1);
assert_eq!(rig.metrics.counts().filtered, 2);
let (mut published, mut replies) = (Vec::new(), Vec::new());
rig.wait_for(&mut published, &mut replies, |p, _| p.len() == 9);
assert_eq!(published, (4..=12).map(RequestId).collect::<Vec<_>>());
assert_eq!(
filtered_ids(&replies),
(1..=3).map(RequestId).collect::<Vec<_>>()
);
assert!(rig.disconnects().is_empty(), "the connection is kept");
rig.wait_for_parked(0);
assert_eq!(rig.metrics.parks(), 1);
assert_eq!(rig.metrics.counts().filtered, 3);
client
.write_all(&request_frame(13, &[0x03]))
.expect("client write");
rig.wait_for(&mut published, &mut replies, |p, _| p.len() == 10);
assert_eq!(published.last(), Some(&RequestId(13)));
drop(client);
rig.stop();
}
#[test]
fn repeated_parks_lose_and_repeat_nothing() {
const REQUESTS: u64 = 600;
let mut rig = Rig::start(1024, None);
let (client, _dup) = rig.connect(1);
let writer = std::thread::spawn(move || {
let mut client = client;
for id in 1..=REQUESTS {
let body = if id % 3 == 0 { 0x01 } else { FILTERED };
client
.write_all(&request_frame(id, &[body]))
.expect("client write");
}
client
});
let (mut published, mut replies) = (Vec::new(), Vec::new());
rig.wait_for(&mut published, &mut replies, |p, r| {
(p.len() + r.len()) as u64 >= REQUESTS
});
let _client = writer.join().expect("writer");
assert_eq!(
published,
(1..=REQUESTS)
.filter(|id| id % 3 == 0)
.map(RequestId)
.collect::<Vec<_>>()
);
assert_eq!(
filtered_ids(&replies),
(1..=REQUESTS)
.filter(|id| id % 3 != 0)
.map(RequestId)
.collect::<Vec<_>>()
);
assert!(rig.disconnects().is_empty());
rig.stop();
}
#[test]
fn a_parked_connection_is_not_reaped_as_idle() {
let mut rig = Rig::start(64, Some(Duration::from_millis(100)));
let (mut client, _dup) = rig.connect(1);
park(&mut rig, &mut client);
std::thread::sleep(Duration::from_millis(2_500));
assert!(rig.disconnects().is_empty(), "reaped while parked");
let (mut published, mut replies) = (Vec::new(), Vec::new());
rig.wait_for(&mut published, &mut replies, |p, _| p.len() == 5);
let (mut other, _other_dup) = rig.connect(2);
let deadline = Instant::now() + Duration::from_secs(10);
let mut disconnects = Vec::new();
let mut id = 100;
while disconnects.is_empty() {
assert!(Instant::now() < deadline, "never reaped once resumed");
id += 1;
other.write_all(&request_frame(id, &[0x01])).expect("write");
rig.take_published();
disconnects.extend(rig.disconnects());
std::thread::sleep(Duration::from_millis(10));
}
assert_eq!(disconnects, [1], "the resumed connection, not the busy one");
drop((client, other));
rig.stop();
}
#[test]
fn a_parked_connection_closed_by_its_retry_is_torn_down_once() {
let mut rig = Rig::start(64, None);
let (mut client, _dup) = rig.connect(1);
park(&mut rig, &mut client);
rig.fence.fence();
let deadline = Instant::now() + Duration::from_secs(10);
let mut disconnects = Vec::new();
while disconnects.is_empty() {
assert!(Instant::now() < deadline, "never torn down");
disconnects.extend(rig.disconnects());
std::thread::sleep(Duration::from_millis(1));
}
std::thread::sleep(Duration::from_millis(50));
disconnects.extend(rig.disconnects());
assert_eq!(disconnects, [1], "torn down once");
assert!(rig.take_published().is_empty());
rig.wait_for_parked(0);
assert_eq!(rig.metrics.parks(), 1);
rig.stop();
}
#[test]
fn a_non_request_in_a_parked_shed_tail_closes_the_connection_once() {
let mut rig = Rig::start(2, None);
let (mut client, _dup) = rig.connect(1);
let mut bytes = Vec::new();
for id in 1..=5 {
bytes.extend(request_frame(id, &[0x01]));
}
bytes.extend(tagged_frame(0x00, &[0x03; 9]));
client.write_all(&bytes).expect("client write");
let (mut published, mut replies) = (Vec::new(), Vec::new());
let deadline = Instant::now() + Duration::from_secs(10);
while published.len() < 2 {
assert!(Instant::now() < deadline, "the ring never filled");
published.extend(
drain(&mut rig.consumer)
.into_iter()
.map(|(_, s)| s.request_id),
);
std::thread::sleep(Duration::from_millis(1));
}
std::thread::sleep(Duration::from_millis(50));
assert!(rig.disconnects().is_empty(), "parked, not closed");
let mut disconnects = Vec::new();
rig.wait_for(&mut published, &mut replies, |_, r| r.len() == 3);
let deadline = Instant::now() + Duration::from_secs(10);
while disconnects.is_empty() {
assert!(Instant::now() < deadline, "never closed");
disconnects.extend(rig.disconnects());
std::thread::sleep(Duration::from_millis(1));
}
std::thread::sleep(Duration::from_millis(50));
disconnects.extend(rig.disconnects());
assert_eq!(disconnects, [1], "torn down once");
assert_eq!(published, [RequestId(1), RequestId(2)]);
assert_eq!(
replies,
(3..=5)
.map(|id| early_reply_for(1, RequestId(id), EarlyKind::Busy))
.collect::<Vec<_>>()
);
rig.stop();
}
#[test]
fn a_parked_connection_torn_down_by_the_response_stage_is_freed_once() {
let mut rig = Rig::start(64, None);
let (mut client, dup) = rig.connect(1);
park(&mut rig, &mut client);
unsafe { libc::shutdown(dup.as_raw_fd(), libc::SHUT_RDWR) };
let deadline = Instant::now() + Duration::from_secs(10);
let mut disconnects = Vec::new();
while disconnects.is_empty() {
assert!(Instant::now() < deadline, "never freed");
rig.take_replies();
rig.take_published();
disconnects.extend(rig.disconnects());
std::thread::sleep(Duration::from_millis(1));
}
std::thread::sleep(Duration::from_millis(50));
disconnects.extend(rig.disconnects());
assert_eq!(disconnects, [1], "freed once");
let (mut next, _next_dup) = rig.connect(2);
next.write_all(&request_frame(1, &[0x01])).expect("write");
let mut published = Vec::new();
rig.wait_for(&mut published, &mut Vec::new(), |p, _| {
p.contains(&RequestId(1))
});
rig.stop();
}
fn early_reply_for(
connection_id: u64,
request_id: RequestId,
kind: EarlyKind<TestReport>,
) -> EarlyReply<TestReport> {
EarlyReply {
connection_id,
request_id,
kind,
}
}
}
#[test]
fn a_ring_that_cannot_be_created_is_a_startup_error() {
let (producer, _consumers) = DisruptorBuilder::<InputSlot<TestEvent>>::new(64)
.add_consumer()
.build(melin_pipeline::wait::WaitStrategy::SpinThenYield);
let (control_tx, _control_rx) = mpsc::channel();
let result = spawn_reader::<TestApp, UnixStream>(
producer,
Arc::new(ByteDecoder),
gate(None, false),
early_channel().0,
Arc::new(IngressMetrics::default()),
policy_claim(),
control_tx,
0,
None,
None,
RingSizing::unchecked(0, 32),
Arc::new(AtomicBool::new(false)),
);
let err = match result {
Ok(_) => panic!("a zero-entry ring cannot be created"),
Err(e) => e,
};
let msg = err.to_string();
assert!(msg.contains("uring-reader"), "{msg}");
assert!(msg.contains("io_uring"), "{msg}");
}
#[test]
fn push_sqe_makes_room_when_the_sq_is_full() {
const ENTRIES: u32 = 8;
const PUSHES: u64 = 3 * ENTRIES as u64;
let mut ring = IoUring::builder()
.setup_cqsize(PUSHES as u32)
.build(ENTRIES)
.expect("create an 8-entry io_uring");
for token in 0..PUSHES {
let nop = opcode::Nop::new().build().user_data(token);
unsafe { push_sqe(&mut ring, &nop, "test NOP") };
}
ring.submit_and_wait(PUSHES as usize)
.expect("submit the remaining NOPs");
let mut seen: Vec<u64> = ring.completion().map(|cqe| cqe.user_data()).collect();
seen.sort_unstable();
assert_eq!(seen, (0..PUSHES).collect::<Vec<_>>());
}
}