use std::os::unix::io::RawFd;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::mpsc;
use std::time::{Duration, Instant};
use io_uring::{IoUring, opcode, types};
use rustc_hash::FxHashMap;
use tracing::{debug, error};
use melin_pipeline::ring;
use melin_pipeline::wait::WaitStrategy;
use crate::ack_policy::{Blocker, CursorView, EvalStatus, MAX_CLUSTER_SIZE, Policy};
use crate::batch_boundary::{AtBoundary, BatchBoundary};
pub(crate) use crate::durability_gate::GatePolicy;
use crate::durability_gate::{Admission, Backing, BatchEnds, DurabilityGate, GateInputs, GateWait};
use crate::early_reply::{EarlyReply, EarlyReplyQueue, Registered};
use crate::replication::ReplicationMetrics;
use melin_app::Application;
use melin_app::amortized_timer::AmortizedTimer;
use melin_transport_core::pipeline::{OutputSlot, StageUtilization};
#[cfg(feature = "latency-trace")]
use melin_transport_core::trace;
use melin_transport_core::{DurableWireSeqCursor, WireSeq};
use melin_wire_protocol::control::TransportResponse;
use melin_wire_protocol::control_codec;
use crate::connection_limit::{RingSizing, ring_setup_error};
use crate::response_frame::{
EarlyReplyBuf, EncodeBuf, MAX_APP_FRAME, MAX_EARLY_REPLY, encode_early_reply,
encode_slot_payload,
};
const MAX_BATCH: usize = 1024;
const MAX_SEND_BUF: usize = 64 * 1024;
const BLOCKED_RETRY_INTERVAL: Duration = Duration::from_micros(100);
const FLUSH_BYTES_THRESHOLD: usize = 1400;
const FLUSH_SLOT_INTERVAL: usize = 256;
#[derive(Default)]
struct FlushCadence {
slots_since_flush: usize,
due: bool,
}
impl FlushCadence {
#[inline]
fn on_append(&mut self, buffered_bytes: usize) {
self.slots_since_flush += 1;
self.due |= buffered_bytes >= FLUSH_BYTES_THRESHOLD;
}
#[inline]
fn is_due(&self) -> bool {
self.due || self.slots_since_flush >= FLUSH_SLOT_INTERVAL
}
#[inline]
fn on_flush(&mut self) {
self.due = false;
self.slots_since_flush = 0;
}
}
const BLOCKED_SEND_TIMEOUT: Duration = Duration::from_secs(5);
pub(crate) use crate::ControlEvent;
pub type ResponseEncoderArc<A> = Arc<
dyn melin_app::encoder::ResponseEncoder<
Report = <A as Application>::Report,
Query = <A as Application>::QueryResponse,
>,
>;
pub struct Response<A: Application> {
pub journal_persisted_wire_seq: DurableWireSeqCursor,
pub policy: GatePolicy,
pub replication_metrics: Option<Arc<ReplicationMetrics>>,
pub replica_active: Option<[Arc<AtomicBool>; 2]>,
pub heartbeat_interval: Option<Duration>,
pub wait: WaitStrategy,
pub utilization: Arc<StageUtilization>,
pub encoder: ResponseEncoderArc<A>,
pub fence_state: Arc<melin_transport_core::fence::FenceState>,
pub active_connections: Arc<AtomicU64>,
pub early_replies: EarlyReplyQueue<A::Report>,
pub ring_sizing: RingSizing,
pub ready: Option<mpsc::SyncSender<std::io::Result<()>>>,
pub degraded_release: Option<crate::halt::DegradedRelease>,
#[cfg(test)]
pub pause_after_control_drain: Option<Box<dyn FnMut() + Send>>,
}
struct ConnectionEntry {
fd: RawFd,
_owner: Box<dyn Send>,
send_buf: Vec<u8>,
last_send: Instant,
blocked_since: Option<Instant>,
last_send_attempt: Instant,
dirty: bool,
}
struct Egress {
ring: IoUring,
connections: FxHashMap<u64, ConnectionEntry>,
dirty: Vec<u64>,
to_remove: Vec<u64>,
cqes: Vec<(u64, i32)>,
cadence: FlushCadence,
active_connections: Arc<AtomicU64>,
#[cfg(feature = "latency-trace")]
pending_e2e: Vec<(u64, trace::MonoTraceInstant)>,
#[cfg(feature = "latency-trace")]
server_e2e_rec: trace::StageRecorder,
#[cfg(feature = "tick-to-trade")]
egress_rec: trace::StageRecorder,
}
impl Egress {
fn new(
ring: IoUring,
ring_entries: u32,
active_connections: Arc<AtomicU64>,
#[cfg(feature = "latency-trace")] server_e2e_rec: trace::StageRecorder,
#[cfg(feature = "tick-to-trade")] egress_rec: trace::StageRecorder,
) -> Self {
Self {
ring,
connections: FxHashMap::with_capacity_and_hasher(256, Default::default()),
dirty: Vec::with_capacity(256),
to_remove: Vec::new(),
cqes: Vec::with_capacity(ring_entries as usize),
cadence: FlushCadence::default(),
active_connections,
#[cfg(feature = "latency-trace")]
pending_e2e: Vec::with_capacity(MAX_BATCH),
#[cfg(feature = "latency-trace")]
server_e2e_rec,
#[cfg(feature = "tick-to-trade")]
egress_rec,
}
}
fn send_dirty(&mut self) {
flush_sends(
&mut self.ring,
&mut self.connections,
&mut self.dirty,
&mut self.to_remove,
&mut self.cqes,
);
self.cadence.on_flush();
}
fn flush(&mut self) {
if self.dirty.is_empty() {
self.cadence.on_flush();
return;
}
#[cfg(feature = "tick-to-trade")]
let egress_start = trace::mono_trace_ns();
self.send_dirty();
#[cfg(feature = "tick-to-trade")]
self.egress_rec
.record_elapsed(egress_start, trace::mono_trace_ns());
#[cfg(feature = "latency-trace")]
close_e2e_samples(&mut self.pending_e2e, &mut self.server_e2e_rec);
self.teardown_flush_failures();
}
fn teardown_flush_failures(&mut self) {
for conn_id in self.to_remove.drain(..) {
if let Some(entry) = self.connections.remove(&conn_id) {
teardown_dropped(entry, &self.active_connections);
}
}
}
fn drop_overflowed(&mut self) {
#[cfg(feature = "latency-trace")]
discard_e2e_samples(&mut self.pending_e2e, &self.to_remove);
for conn_id in self.to_remove.drain(..) {
if let Some(entry) = self.connections.remove(&conn_id) {
teardown_dropped(entry, &self.active_connections);
}
unmark_dirty(conn_id, &mut self.dirty);
}
}
#[inline]
fn flush_if_due(&mut self) {
if !self.cadence.is_due() {
return;
}
self.drop_overflowed();
self.flush();
}
fn drain_control(&mut self, control_rx: &mpsc::Receiver<ControlEvent>) {
while let Ok(event) = control_rx.try_recv() {
match event {
ControlEvent::Connected {
connection_id,
fd,
writer,
} => {
let owner: Box<dyn Send> = Box::new(writer);
self.connections.insert(
connection_id,
ConnectionEntry {
fd,
_owner: owner,
send_buf: Vec::with_capacity(4096),
last_send: Instant::now(),
blocked_since: None,
last_send_attempt: Instant::now(),
dirty: false,
},
);
}
ControlEvent::Disconnected { connection_id } => {
if self.connections.remove(&connection_id).is_some() {
self.active_connections.fetch_sub(1, Ordering::Relaxed);
}
unmark_dirty(connection_id, &mut self.dirty);
#[cfg(feature = "latency-trace")]
discard_e2e_samples(&mut self.pending_e2e, &[connection_id]);
}
}
}
}
fn release_early_replies<R: Copy, Q: Copy>(
&mut self,
registered: &mut Registered<'_, R>,
at: &AtBoundary<'_>,
encoder: &dyn melin_app::encoder::ResponseEncoder<Report = R, Query = Q>,
early_buf: &mut EarlyReplyBuf,
now: impl Fn() -> Instant,
) -> usize {
let mut stamp = None;
registered.drain(at, |reply| {
let now = *stamp.get_or_insert_with(&now);
append_early_reply(
reply,
encoder,
&mut self.connections,
early_buf,
now,
&mut self.dirty,
&mut self.to_remove,
&mut self.cadence,
);
self.flush_if_due();
})
}
}
struct GateWaitHook<'a, 'q, R: Copy, Q: Copy> {
egress: &'a mut Egress,
at_boundary: Option<AtBoundary<'a>>,
registered: &'a mut Registered<'q, R>,
control_rx: &'a mpsc::Receiver<ControlEvent>,
encoder: &'a dyn melin_app::encoder::ResponseEncoder<Report = R, Query = Q>,
early_buf: &'a mut EarlyReplyBuf,
}
impl<R: Copy, Q: Copy> GateWait for GateWaitHook<'_, '_, R, Q> {
fn before_wait(&mut self) {
self.egress.flush();
}
fn while_waiting(&mut self) {
let Some(at) = &self.at_boundary else {
return;
};
let egress = &mut *self.egress;
let control_rx = self.control_rx;
self.registered
.reregister(|| egress.drain_control(control_rx));
if egress.release_early_replies(
self.registered,
at,
self.encoder,
self.early_buf,
Instant::now,
) > 0
{
egress.drop_overflowed();
egress.flush();
}
}
}
pub fn run<A: Application>(
mut consumer: ring::Consumer<OutputSlot<A::Report, A::QueryResponse>>,
control_rx: mpsc::Receiver<ControlEvent>,
config: Response<A>,
shutdown: &AtomicBool,
) -> bool {
let Response {
journal_persisted_wire_seq,
policy,
replication_metrics,
replica_active,
heartbeat_interval,
wait,
utilization,
encoder,
fence_state,
active_connections,
mut early_replies,
ring_sizing,
ready,
degraded_release,
#[cfg(test)]
mut pause_after_control_drain,
} = config;
let ring_entries = ring_sizing.ring_entries();
let ring: IoUring = match IoUring::builder().setup_single_issuer().build(ring_entries) {
Ok(ring) => ring,
Err(e) => {
let err = ring_setup_error("response", ring_entries, e);
match ready {
Some(ready) => {
let _ = ready.send(Err(err));
}
None => error!(error = %err, "response stage cannot start"),
}
return false;
}
};
if let Some(ready) = ready
&& ready.send(Ok(())).is_err()
{
return false;
}
let mut gate = DurabilityGate::new(
GateInputs {
journal_persisted_wire_seq,
replication_metrics,
replica_active,
utilization: Arc::clone(&utilization),
wait,
degraded_release,
},
policy,
"response",
);
let mut encode_buf: EncodeBuf = [0u8; MAX_APP_FRAME];
let mut early_buf: EarlyReplyBuf = [0u8; MAX_EARLY_REPLY];
const IDLE_HOUSEKEEPING_INTERVAL: Duration = Duration::from_millis(10);
#[cfg(feature = "latency-trace")]
let mut spsc_rec =
trace::register_stage("response: SPSC wakeup (matching publish → response consume)");
#[cfg(feature = "latency-trace")]
let mut dispatch_rec = trace::register_stage("response: dispatch (consume → socket write)");
#[cfg(feature = "latency-trace")]
let server_e2e_rec = trace::register_stage("server e2e (reader recv → response flush)");
#[cfg(feature = "tick-to-trade")]
let mut journal_wait_rec =
trace::register_stage("response: journal-wait (match_complete → journal cursor crossed)");
#[cfg(feature = "tick-to-trade")]
let mut replica_wait_rec = trace::register_stage(
"response: replica-wait (match_complete → replication cursor crossed)",
);
#[cfg(feature = "tick-to-trade")]
let mut encode_rec = trace::register_stage("response: encode (per-kind wire encoding)");
#[cfg(feature = "tick-to-trade")]
let egress_rec = trace::register_stage("response: egress (flush_sends elapsed)");
#[cfg(feature = "latency-trace")]
let mut last_stats_flush = Instant::now();
let mut egress = Egress::new(
ring,
ring_entries,
active_connections,
#[cfg(feature = "latency-trace")]
server_e2e_rec,
#[cfg(feature = "tick-to-trade")]
egress_rec,
);
let heartbeat_wire_frame = {
let mut buf = [0u8; 8];
let written =
control_codec::encode_transport_response(&TransportResponse::Heartbeat, &mut buf)
.expect("heartbeat encodes");
buf[..written].to_vec()
};
let mut batch_ends = BatchEnds::new();
let mut last_heartbeat_scan = Instant::now();
let mut idle_housekeeping_timer = AmortizedTimer::new();
let mut waiter = wait.waiter();
let mut busy_count: u64 = 0;
let mut idle_count: u64 = 0;
let mut boundary = BatchBoundary::new();
'run: loop {
if shutdown.load(Ordering::Relaxed) {
let flush = !fence_state.is_fenced();
if flush && !egress.dirty.is_empty() {
egress.send_dirty();
#[cfg(feature = "latency-trace")]
close_e2e_samples(&mut egress.pending_e2e, &mut egress.server_e2e_rec);
}
utilization.busy.store(busy_count, Ordering::Relaxed);
utilization.idle.store(idle_count, Ordering::Relaxed);
#[cfg(feature = "pipeline-stats")]
print_utilization("response", busy_count, idle_count);
return true;
}
let slots = consumer.read_contiguous(MAX_BATCH);
let mut registered = early_replies.register(|| egress.drain_control(&control_rx));
#[cfg(test)]
if let Some(pause) = pause_after_control_drain.as_mut() {
pause();
}
if slots.is_empty() {
if let Some(at) = boundary.at_boundary() {
egress.release_early_replies(
&mut registered,
&at,
&*encoder,
&mut early_buf,
Instant::now,
);
egress.drop_overflowed();
}
egress.flush();
if idle_housekeeping_timer
.tick(
IDLE_HOUSEKEEPING_INTERVAL,
waiter.spinning() && waiter.past_spin_budget(),
)
.is_some()
{
let now = Instant::now();
if let Some(interval) = heartbeat_interval
&& now.duration_since(last_heartbeat_scan) >= Duration::from_secs(1)
{
last_heartbeat_scan = now;
for (&conn_id, entry) in egress.connections.iter_mut() {
if heartbeat_due(entry, now, interval, heartbeat_wire_frame.len()) {
entry.send_buf.extend_from_slice(&heartbeat_wire_frame);
entry.last_send = now;
mark_dirty(entry, conn_id, &mut egress.dirty);
}
}
if !egress.dirty.is_empty() {
egress.send_dirty();
egress.teardown_flush_failures();
}
}
gate.idle_recheck(now);
#[cfg(feature = "latency-trace")]
if now.duration_since(last_stats_flush) >= trace::IDLE_FLUSH_INTERVAL {
last_stats_flush = now;
spsc_rec.flush();
dispatch_rec.flush();
egress.server_e2e_rec.flush();
#[cfg(feature = "tick-to-trade")]
{
journal_wait_rec.flush();
replica_wait_rec.flush();
encode_rec.flush();
egress.egress_rec.flush();
}
}
}
idle_count += 1;
if idle_count.is_multiple_of(1024) {
utilization.busy.store(busy_count, Ordering::Relaxed);
utilization.idle.store(idle_count, Ordering::Relaxed);
}
waiter.idle();
continue;
}
waiter.reset();
busy_count += 1;
#[cfg(feature = "latency-trace")]
let consume_ts = trace::mono_trace_ns();
let batch_now = Instant::now();
for slot in slots {
#[cfg(feature = "latency-trace")]
spsc_rec.record_elapsed(slot.match_complete_ts, consume_ts);
let at_boundary = boundary.at_boundary();
if let Some(at) = &at_boundary {
egress.release_early_replies(
&mut registered,
at,
&*encoder,
&mut early_buf,
|| batch_now,
);
}
#[cfg(feature = "tick-to-trade")]
let mut gate_tracker = GateCrossTracker::new(slot.wire_seq);
let admission = gate.admit(
slot,
shutdown,
#[cfg(feature = "tick-to-trade")]
&mut gate_tracker,
&mut GateWaitHook {
egress: &mut egress,
at_boundary,
registered: &mut registered,
control_rx: &control_rx,
encoder: &*encoder,
early_buf: &mut early_buf,
},
);
let (destination, backing) = match admission {
Admission::Reply {
connection_id,
backing,
} => (Some(connection_id), backing),
Admission::GateOnly => (None, Backing::Policy),
Admission::Shutdown => continue 'run,
};
boundary.handled(slot.is_last_in_request);
#[cfg(feature = "tick-to-trade")]
if let Some(ts) = gate_tracker.journal_crossed() {
journal_wait_rec.record_elapsed(slot.match_complete_ts, ts);
}
#[cfg(feature = "tick-to-trade")]
if let Some(ts) = gate_tracker.replica_crossed() {
replica_wait_rec.record_elapsed(slot.match_complete_ts, ts);
}
if let Some((connection_id, entry)) =
destination.and_then(|id| egress.connections.get_mut(&id).map(|entry| (id, entry)))
{
let payload_result = encode_slot_payload(&slot.payload, &*encoder, &mut encode_buf);
let trailer: &[u8] = if slot.is_last_in_request {
gate.batch_end(backing, &mut batch_ends, slot.request_id)
} else {
&[]
};
#[cfg(feature = "tick-to-trade")]
let encode_start = trace::mono_trace_ns();
let _outcome = append_frames(
payload_result,
trailer,
connection_id,
entry,
&encode_buf,
batch_now,
&mut egress.dirty,
&mut egress.to_remove,
);
#[cfg(feature = "tick-to-trade")]
encode_rec.record_elapsed(encode_start, trace::mono_trace_ns());
#[cfg(feature = "latency-trace")]
if slot.is_last_in_request && matches!(_outcome, AppendOutcome::Continue) {
egress.pending_e2e.push((connection_id, slot.recv_ts));
}
egress.cadence.on_append(entry.send_buf.len());
} else if let Some(connection_id) = destination.filter(|&id| id != 0) {
debug!(
connection_id,
wire_seq = slot.wire_seq,
"reply dropped: connection not registered"
);
}
egress.flush_if_due();
}
consumer.commit();
egress.drop_overflowed();
gate.after_batch();
#[cfg(feature = "latency-trace")]
dispatch_rec.record_elapsed(consume_ts, trace::mono_trace_ns());
}
}
#[cfg(feature = "latency-trace")]
fn close_e2e_samples(
pending: &mut Vec<(u64, trace::MonoTraceInstant)>,
rec: &mut trace::StageRecorder,
) {
let flushed_at = trace::mono_trace_ns();
for (_, recv_ts) in pending.drain(..) {
rec.record_elapsed(recv_ts, flushed_at);
}
}
#[cfg(feature = "latency-trace")]
fn discard_e2e_samples(pending: &mut Vec<(u64, trace::MonoTraceInstant)>, dropped: &[u64]) {
if dropped.is_empty() {
return;
}
pending.retain(|(conn_id, _)| !dropped.contains(conn_id));
}
#[inline]
pub(crate) fn slot_needs_gate<R: Copy, Q: Copy>(
slot: &OutputSlot<R, Q>,
cached_durable_pos: u64,
) -> bool {
cached_durable_pos < slot.wire_seq
}
fn teardown_dropped(entry: ConnectionEntry, active_connections: &AtomicU64) {
unsafe {
libc::shutdown(entry.fd, libc::SHUT_RDWR);
}
active_connections.fetch_sub(1, Ordering::Relaxed);
}
#[derive(Clone, Copy)]
enum AppendOutcome {
Continue,
ConnectionDropped,
}
#[inline]
fn mark_dirty(entry: &mut ConnectionEntry, connection_id: u64, dirty: &mut Vec<u64>) {
if !entry.dirty {
entry.dirty = true;
dirty.push(connection_id);
}
}
fn unmark_dirty(connection_id: u64, dirty: &mut Vec<u64>) {
dirty.retain(|&id| id != connection_id);
}
fn heartbeat_due(
entry: &ConnectionEntry,
now: Instant,
interval: Duration,
frame_len: usize,
) -> bool {
now.duration_since(entry.last_send) >= interval
&& entry.blocked_since.is_none()
&& entry.send_buf.len() + frame_len <= MAX_SEND_BUF
}
#[allow(clippy::too_many_arguments)]
fn append_frames(
payload: Option<Result<usize, &'static str>>,
trailer: &[u8],
connection_id: u64,
entry: &mut ConnectionEntry,
encode_buf: &[u8],
batch_now: Instant,
dirty_connections: &mut Vec<u64>,
to_remove: &mut Vec<u64>,
) -> AppendOutcome {
let written = match payload {
Some(Ok(n)) => n,
Some(Err(reason)) => {
tracing::error!(connection_id, reason, "encode error");
0
}
None => 0,
};
let total = written + trailer.len();
if total == 0 {
return AppendOutcome::Continue;
}
if entry.send_buf.len() + total > MAX_SEND_BUF {
debug!(
connection_id,
send_buf_len = entry.send_buf.len(),
"send buffer exceeded limit, dropping connection"
);
to_remove.push(connection_id);
return AppendOutcome::ConnectionDropped;
}
entry.send_buf.extend_from_slice(&encode_buf[..written]);
entry.send_buf.extend_from_slice(trailer);
entry.last_send = batch_now;
mark_dirty(entry, connection_id, dirty_connections);
AppendOutcome::Continue
}
#[allow(clippy::too_many_arguments)]
fn append_early_reply<R: Copy, Q: Copy>(
reply: EarlyReply<R>,
encoder: &dyn melin_app::encoder::ResponseEncoder<Report = R, Query = Q>,
connections: &mut FxHashMap<u64, ConnectionEntry>,
early_buf: &mut EarlyReplyBuf,
now: Instant,
dirty_connections: &mut Vec<u64>,
to_remove: &mut Vec<u64>,
flush: &mut FlushCadence,
) {
let Some(entry) = connections.get_mut(&reply.connection_id) else {
debug!(
connection_id = reply.connection_id,
"early reply dropped: connection not registered"
);
return;
};
let written = encode_early_reply(&reply, encoder, early_buf);
append_frames(
Some(Ok(written)),
&[],
reply.connection_id,
entry,
early_buf,
now,
dirty_connections,
to_remove,
);
flush.on_append(entry.send_buf.len());
}
fn flush_sends(
ring: &mut IoUring,
connections: &mut FxHashMap<u64, ConnectionEntry>,
dirty: &mut Vec<u64>,
to_remove: &mut Vec<u64>,
cqes: &mut Vec<(u64, i32)>,
) {
let now = Instant::now();
let mut pending: usize = 0;
for &conn_id in dirty.iter() {
let Some(entry) = connections.get_mut(&conn_id) else {
continue;
};
if entry.send_buf.is_empty() {
continue;
}
if let Some(blocked_at) = entry.blocked_since {
if now.duration_since(blocked_at) >= BLOCKED_SEND_TIMEOUT {
debug!(
connection_id = conn_id,
pending_bytes = entry.send_buf.len(),
blocked_ms = now.duration_since(blocked_at).as_millis() as u64,
"socket blocked past timeout, dropping connection"
);
to_remove.push(conn_id);
continue;
}
if now.duration_since(entry.last_send_attempt) < BLOCKED_RETRY_INTERVAL {
continue;
}
}
entry.last_send_attempt = now;
let sqe = opcode::Send::new(
types::Fd(entry.fd),
entry.send_buf.as_ptr(),
entry.send_buf.len() as u32,
)
.flags(libc::MSG_DONTWAIT)
.build()
.user_data(conn_id);
unsafe {
ring.submission().push(&sqe).expect(
"io_uring SQ full: a flush submits one SEND per connection, and the ring \
is sized above --max-connections",
);
}
pending += 1;
}
if pending > 0 {
loop {
match ring.submit_and_wait(pending) {
Ok(_) => break,
Err(ref e) if e.raw_os_error() == Some(libc::EINTR) => continue,
Err(e) => {
error!(error = %e, "io_uring submit_and_wait failed in response stage");
panic!("response stage io_uring submit failed: {e}");
}
}
}
cqes.clear();
cqes.extend(ring.completion().map(|cqe| (cqe.user_data(), cqe.result())));
for &(conn_id, result) in cqes.iter() {
let Some(entry) = connections.get_mut(&conn_id) else {
continue;
};
if result == -libc::EAGAIN {
if entry.blocked_since.is_none() {
entry.blocked_since = Some(now);
}
continue;
}
if result < 0 {
debug!(
connection_id = conn_id,
error = result,
"send error, dropping connection"
);
to_remove.push(conn_id);
continue;
}
let sent = result as usize;
if sent == 0 {
if entry.blocked_since.is_none() {
entry.blocked_since = Some(now);
}
} else if sent >= entry.send_buf.len() {
entry.send_buf.clear();
entry.blocked_since = None;
} else {
entry.send_buf.drain(..sent);
entry.blocked_since = Some(now);
}
}
}
dirty.retain(|conn_id| {
let keep = !to_remove.contains(conn_id)
&& connections
.get(conn_id)
.is_some_and(|e| !e.send_buf.is_empty());
if !keep && let Some(entry) = connections.get_mut(conn_id) {
entry.dirty = false;
}
keep
});
}
#[inline]
fn with_cursor_view<R>(
journal_pos: WireSeq,
metrics: Option<&ReplicationMetrics>,
replica_active: Option<&[Arc<AtomicBool>; 2]>,
f: impl FnOnce(&CursorView<'_>) -> R,
) -> R {
let mut nodes = [[0u64; 2]; MAX_CLUSTER_SIZE as usize];
nodes[0] = [u64::MAX, journal_pos.get()];
let mut len = 1;
if let (Some(m), Some(active)) = (metrics, replica_active) {
for (i, slot_active) in active.iter().enumerate() {
if !slot_active.load(Ordering::Acquire) {
continue;
}
let in_mem = m.in_memory_sequence[i].load(Ordering::Acquire);
let persisted = m.acked_sequence[i].load(Ordering::Acquire);
nodes[len] = [in_mem, persisted];
len += 1;
}
}
f(&CursorView::new(&nodes[..len]))
}
#[inline]
pub(crate) fn evaluate_durability(
policy: &Policy,
journal_pos: WireSeq,
metrics: Option<&ReplicationMetrics>,
replica_active: Option<&[Arc<AtomicBool>; 2]>,
) -> EvalStatus {
with_cursor_view(journal_pos, metrics, replica_active, |view| {
policy.evaluate_with_status(view)
})
}
#[inline]
pub(crate) fn evaluate_gate(
policy: &Policy,
needed: u64,
journal_pos: WireSeq,
metrics: Option<&ReplicationMetrics>,
replica_active: Option<&[Arc<AtomicBool>; 2]>,
) -> (EvalStatus, Option<Blocker>) {
with_cursor_view(journal_pos, metrics, replica_active, |view| {
let status = policy.evaluate_with_status(view);
let blocker = if status.durable_pos >= needed {
policy.attribute_blocker(view)
} else {
None
};
(status, blocker)
})
}
#[cfg(feature = "tick-to-trade")]
#[inline]
pub(crate) fn policy_replica_cursor(
policy: &Policy,
journal_pos: WireSeq,
metrics: Option<&ReplicationMetrics>,
replica_active: Option<&[Arc<AtomicBool>; 2]>,
) -> Option<u64> {
with_cursor_view(journal_pos, metrics, replica_active, |view| {
policy.replica_gate_cursor(view)
})
}
const DEGRADED_FLAP_HOLD: Duration = Duration::from_secs(1);
pub(crate) struct DegradationLogger {
pending_state: bool,
pending_since: Instant,
pending_logged: bool,
last_log: Option<Instant>,
last_tick: Instant,
}
impl DegradationLogger {
pub(crate) fn new(now: Instant) -> Self {
Self {
pending_state: false,
pending_since: now,
pending_logged: true, last_log: None,
last_tick: now,
}
}
pub(crate) fn new_starting_degraded(now: Instant, policy: &Policy) -> Self {
tracing::warn!(
policy = %policy,
"ack policy starts degraded — fewer connected nodes than the target count"
);
Self {
pending_state: true,
pending_since: now,
pending_logged: true,
last_log: Some(now),
last_tick: now,
}
}
#[inline]
fn accrue(&mut self, utilization: &StageUtilization, prev_degraded: bool, now: Instant) {
if prev_degraded {
let elapsed = now.duration_since(self.last_tick);
utilization
.policy_degraded_nanos
.fetch_add(elapsed.as_nanos() as u64, Ordering::Relaxed);
}
self.last_tick = now;
}
pub(crate) fn reseed(&mut self, utilization: &StageUtilization, now: Instant) {
let prev_state = self.pending_state;
self.accrue(utilization, prev_state, now);
*self = Self::new(now);
}
pub(crate) fn tick(
&mut self,
policy: &Policy,
utilization: &StageUtilization,
degraded_now: bool,
now: Instant,
heartbeat_interval: Duration,
) {
let prev_state = self.pending_state;
self.accrue(utilization, prev_state, now);
utilization
.policy_degraded
.store(degraded_now, Ordering::Relaxed);
if degraded_now != self.pending_state {
self.pending_state = degraded_now;
self.pending_since = now;
self.pending_logged = false;
return;
}
if !self.pending_logged && now.duration_since(self.pending_since) >= DEGRADED_FLAP_HOLD {
if degraded_now {
tracing::warn!(
policy = %policy,
"ack policy operating degraded — fewer connected nodes than the target count, response gate stalled until the cluster recovers or the policy is swapped"
);
} else {
tracing::info!(
policy = %policy,
"ack policy returned to target shape"
);
}
self.pending_logged = true;
self.last_log = Some(now);
return;
}
if degraded_now
&& self.pending_logged
&& self
.last_log
.is_none_or(|t| now.duration_since(t) >= heartbeat_interval)
{
tracing::warn!(
policy = %policy,
"ack policy still degraded — fewer connected nodes than the target count"
);
self.last_log = Some(now);
}
}
}
#[cfg(feature = "tick-to-trade")]
pub(crate) struct GateCrossTracker {
needed: u64,
journal_crossed_ts: Option<trace::MonoTraceInstant>,
replica_crossed_ts: Option<trace::MonoTraceInstant>,
journal_was_below: bool,
replica_was_below: bool,
first: bool,
}
#[cfg(feature = "tick-to-trade")]
impl GateCrossTracker {
pub(crate) fn new(needed: u64) -> Self {
Self {
needed,
journal_crossed_ts: None,
replica_crossed_ts: None,
journal_was_below: false,
replica_was_below: false,
first: true,
}
}
pub(crate) fn observe(
&mut self,
journal_pos: u64,
replica_pos: Option<u64>,
now_ns: trace::MonoTraceInstant,
) {
if self.first {
self.journal_was_below = journal_pos < self.needed;
self.replica_was_below = replica_pos.is_some_and(|p| p < self.needed);
self.first = false;
}
if self.journal_was_below && self.journal_crossed_ts.is_none() && journal_pos >= self.needed
{
self.journal_crossed_ts = Some(now_ns);
}
if self.replica_was_below
&& self.replica_crossed_ts.is_none()
&& replica_pos.is_some_and(|p| p >= self.needed)
{
self.replica_crossed_ts = Some(now_ns);
}
}
pub(crate) fn journal_crossed(&self) -> Option<trace::MonoTraceInstant> {
self.journal_crossed_ts
}
pub(crate) fn replica_crossed(&self) -> Option<trace::MonoTraceInstant> {
self.replica_crossed_ts
}
}
#[cfg(feature = "pipeline-stats")]
fn print_utilization(stage: &str, busy: u64, idle: u64) {
let total = busy + idle;
if total == 0 {
tracing::info!(stage, "no iterations recorded");
return;
}
let pct = (busy as f64 / total as f64) * 100.0;
tracing::info!(
stage,
pct_busy = format_args!("{pct:.2}%"),
busy,
idle,
total,
"pipeline utilization",
);
}
#[cfg(test)]
pub(crate) mod tests {
#[cfg(feature = "tick-to-trade")]
use super::GateCrossTracker;
use super::{
Blocker, DegradationLogger, OutputSlot, WireSeq, evaluate_durability, evaluate_gate,
slot_needs_gate,
};
use crate::ack_policy::{AckPolicy, Clause, Level, Policy};
use crate::replication::ReplicationMetrics;
#[cfg(feature = "latency-trace")]
#[test]
fn e2e_samples_survive_until_a_flush_closes_them() {
let mut pending = vec![(1u64, 10u64), (2, 20), (1, 30)];
super::discard_e2e_samples(&mut pending, &[]);
assert_eq!(pending.len(), 3, "an empty drop list must lose nothing");
let mut rec = melin_transport_core::trace::register_stage("test::response_e2e_close");
super::close_e2e_samples(&mut pending, &mut rec);
assert!(pending.is_empty(), "a flush closes the whole queue");
}
#[cfg(feature = "latency-trace")]
#[test]
fn e2e_samples_for_a_dropped_connection_are_discarded() {
let mut pending = vec![(1u64, 10u64), (2, 20), (1, 30), (3, 40)];
super::discard_e2e_samples(&mut pending, &[1, 3]);
assert_eq!(
pending,
vec![(2, 20)],
"only the surviving connection's samples remain"
);
super::discard_e2e_samples(&mut pending, &[2]);
assert!(pending.is_empty());
}
fn slot(wire_seq: u64) -> OutputSlot<(), ()> {
OutputSlot {
wire_seq,
..Default::default()
}
}
#[test]
fn slot_gates_until_its_own_sequence_is_durable() {
assert!(slot_needs_gate(&slot(10), 0));
assert!(slot_needs_gate(&slot(10), 9));
}
#[test]
fn slot_clears_at_its_own_sequence_not_the_batch_maximum() {
assert!(!slot_needs_gate(&slot(10), 10));
assert!(!slot_needs_gate(&slot(10), 11));
}
fn parse(s: &str) -> Result<Policy, String> {
let mut clauses = Vec::new();
for raw in s.split("&&") {
let token = raw.trim();
let (lvl, rhs) = token
.split_once(">=")
.ok_or_else(|| format!("clause `{token}` missing `>=`"))?;
let level = match lvl.trim() {
"persisted" => Level::Persisted,
"in_memory" => Level::InMemory,
other => return Err(format!("unknown level `{other}`")),
};
let count: u8 = rhs.trim().parse().map_err(|e| format!("bad count: {e}"))?;
clauses.push(Clause { count, level });
}
Policy::new(clauses).map_err(|e| e.to_string())
}
use melin_transport_core::pipeline::StageUtilization;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{Duration, Instant};
fn metrics(slot0: (u64, u64), slot1: (u64, u64)) -> Arc<ReplicationMetrics> {
let m = Arc::new(ReplicationMetrics::default());
m.in_memory_sequence[0].store(slot0.0, Ordering::Relaxed);
m.acked_sequence[0].store(slot0.1, Ordering::Relaxed);
m.in_memory_sequence[1].store(slot1.0, Ordering::Relaxed);
m.acked_sequence[1].store(slot1.1, Ordering::Relaxed);
m
}
fn flags(slot0_active: bool, slot1_active: bool) -> [Arc<AtomicBool>; 2] {
[
Arc::new(AtomicBool::new(slot0_active)),
Arc::new(AtomicBool::new(slot1_active)),
]
}
fn both_active() -> [Arc<AtomicBool>; 2] {
flags(true, true)
}
#[test]
fn standalone_persisted_one_gates_on_journal() {
let p = parse("persisted>=1").unwrap();
assert_eq!(
evaluate_durability(&p, WireSeq::new(500), None, None).durable_pos,
500
);
}
#[test]
fn standalone_strict_persisted_two_never_opens() {
let p = parse("persisted>=2").unwrap();
let r = evaluate_durability(&p, WireSeq::new(500), None, None);
assert_eq!(r.durable_pos, 0);
assert!(
r.degraded,
"policy structurally unsatisfiable on this shape → degraded",
);
}
#[test]
fn quorum_both_replicas_ahead_of_journal() {
let p = parse("persisted>=2").unwrap();
let m = metrics((100, 100), (120, 120));
let a = both_active();
assert_eq!(
evaluate_durability(&p, WireSeq::new(50), Some(&m), Some(&a)).durable_pos,
100
);
}
#[test]
fn quorum_journal_ahead_of_both_replicas() {
let p = parse("persisted>=2").unwrap();
let m = metrics((100, 100), (120, 120));
let a = both_active();
assert_eq!(
evaluate_durability(&p, WireSeq::new(500), Some(&m), Some(&a)).durable_pos,
120
);
}
#[test]
fn quorum_journal_between_slow_and_fast_replica() {
let p = parse("persisted>=2").unwrap();
let m = metrics((50, 50), (200, 200));
let a = both_active();
assert_eq!(
evaluate_durability(&p, WireSeq::new(150), Some(&m), Some(&a)).durable_pos,
150
);
}
#[test]
fn ram_quorum_gates_on_replica_memory_not_on_any_disk() {
let p = AckPolicy::Ram.to_policy();
let m = metrics((100, 0), (120, 0));
let a = both_active();
let r = evaluate_durability(&p, WireSeq::new(0), Some(&m), Some(&a));
assert_eq!(r.durable_pos, 120);
assert!(!r.degraded);
}
#[test]
fn ram_quorum_stalls_with_no_replica_connected() {
let p = AckPolicy::Ram.to_policy();
let r = evaluate_durability(&p, WireSeq::new(500), None, None);
assert_eq!(r.durable_pos, 0);
assert!(r.degraded);
}
#[test]
fn single_replica_strict_persisted_two_requires_both_survivors() {
let p = parse("persisted>=2").unwrap();
let m = metrics((100, 100), (999, 999)); let a = flags(true, false);
assert_eq!(
evaluate_durability(&p, WireSeq::new(50), Some(&m), Some(&a)).durable_pos,
50
);
assert_eq!(
evaluate_durability(&p, WireSeq::new(200), Some(&m), Some(&a)).durable_pos,
100
);
}
#[test]
fn single_replica_persisted_two_requires_both_survivors() {
let p = parse("persisted>=2").unwrap();
let m = metrics((100, 100), (999, 999));
let a = flags(true, false);
let r = evaluate_durability(&p, WireSeq::new(50), Some(&m), Some(&a));
assert_eq!(r.durable_pos, 50);
assert!(!r.degraded);
}
#[test]
fn both_replicas_disconnected_strict_stalls() {
let p = parse("persisted>=2").unwrap();
let m = metrics((999, 999), (999, 999));
let a = flags(false, false);
assert_eq!(
evaluate_durability(&p, WireSeq::new(500), Some(&m), Some(&a)).durable_pos,
0
);
}
#[test]
fn both_replicas_disconnected_strict_stalls_and_flags_degraded() {
let p = parse("persisted>=2").unwrap();
let m = metrics((999, 999), (999, 999));
let a = flags(false, false);
let r = evaluate_durability(&p, WireSeq::new(500), Some(&m), Some(&a));
assert_eq!(r.durable_pos, 0);
assert!(r.degraded);
}
#[test]
fn persisted_one_and_in_memory_two() {
let p = parse("persisted>=1 && in_memory>=2").unwrap();
let m = metrics((80, 20), (999, 999));
let a = flags(true, false);
assert_eq!(
evaluate_durability(&p, WireSeq::new(50), Some(&m), Some(&a)).durable_pos,
50
);
}
#[test]
fn journal_at_zero_with_replicas_persisted_one() {
let p = parse("persisted>=1").unwrap();
let m = metrics((100, 100), (200, 200));
let a = both_active();
assert_eq!(
evaluate_durability(&p, WireSeq::new(0), Some(&m), Some(&a)).durable_pos,
200
);
}
#[test]
fn disk_policy_credits_journal_when_replicas_lag() {
let p = parse("persisted>=1").unwrap();
let m = metrics((10, 10), (10, 10));
let a = both_active();
assert_eq!(
evaluate_gate(&p, 500, WireSeq::new(500), Some(&m), Some(&a)).1,
Some(Blocker::Journal)
);
}
#[test]
fn disk_policy_credits_replication_when_a_replica_fsync_leads() {
let p = parse("persisted>=1").unwrap();
let m = metrics((500, 500), (10, 10));
let a = both_active();
assert_eq!(
evaluate_gate(&p, 500, WireSeq::new(10), Some(&m), Some(&a)).1,
Some(Blocker::Replication)
);
}
#[test]
fn disk_and_ram_reads_replica_in_memory_not_persisted() {
let p = parse("persisted>=1 && in_memory>=2").unwrap();
let m = metrics((400, 100), (0, 0));
let a = flags(true, false);
assert_eq!(
evaluate_gate(&p, 300, WireSeq::new(300), Some(&m), Some(&a)).1,
Some(Blocker::Journal)
);
let m = metrics((200, 100), (0, 0));
assert_eq!(
evaluate_gate(&p, 200, WireSeq::new(300), Some(&m), Some(&a)).1,
Some(Blocker::Replication)
);
}
#[test]
fn disk_and_ram_is_satisfied_by_the_faster_replica() {
let p = parse("persisted>=1 && in_memory>=2").unwrap();
let m = metrics((400, 250), (10, 10));
let a = both_active();
assert_eq!(
evaluate_gate(&p, 300, WireSeq::new(300), Some(&m), Some(&a)).1,
Some(Blocker::Journal)
);
}
#[test]
fn two_disks_uses_second_largest_persisted() {
let p = parse("persisted>=2").unwrap();
let m = metrics((400, 400), (10, 10));
let a = both_active();
assert_eq!(
evaluate_durability(&p, WireSeq::new(900), Some(&m), Some(&a)).durable_pos,
400,
"clause takes the second-largest persisted cursor, not the minimum"
);
assert_eq!(
evaluate_gate(&p, 400, WireSeq::new(900), Some(&m), Some(&a)).1,
Some(Blocker::Replication)
);
}
#[test]
fn standalone_credits_the_journal() {
let p = parse("persisted>=1").unwrap();
assert_eq!(
evaluate_gate(&p, 500, WireSeq::new(500), None, None).1,
Some(Blocker::Journal)
);
}
#[test]
fn unsatisfiable_shape_never_opens_so_never_attributes() {
let p = parse("persisted>=2").unwrap();
let (status, blocker) = evaluate_gate(&p, 1, WireSeq::new(500), None, None);
assert!(status.degraded);
assert_eq!(status.durable_pos, 0);
assert_eq!(blocker, None);
}
#[test]
fn closed_gate_yields_no_attribution() {
let p = parse("persisted>=1").unwrap();
let (status, blocker) = evaluate_gate(&p, 501, WireSeq::new(500), None, None);
assert!(!status.degraded);
assert_eq!(status.durable_pos, 500);
assert_eq!(blocker, None);
}
#[test]
fn attribution_skips_disconnected_slots() {
let p = parse("persisted>=1 && in_memory>=2").unwrap();
let m = metrics((100, 100), (999, 999));
let a = flags(true, false);
assert_eq!(
evaluate_gate(&p, 100, WireSeq::new(500), Some(&m), Some(&a)).1,
Some(Blocker::Replication)
);
}
#[test]
fn fresh_cluster_zero_cursors_included_in_view() {
let p = parse("persisted>=2").unwrap();
let m = metrics((0, 0), (0, 0));
let a = both_active();
let r = evaluate_durability(&p, WireSeq::new(0), Some(&m), Some(&a));
assert_eq!(r.durable_pos, 0);
assert!(
!r.degraded,
"all 3 nodes present, view meets clause target — should not flag degraded"
);
}
#[test]
fn race_b1_post_seed_gate_doesnt_freeze_on_reconnect() {
let p = parse("persisted>=2").unwrap();
let m = metrics((480, 480), (999, 999));
let a = flags(true, false);
let r = evaluate_durability(&p, WireSeq::new(500), Some(&m), Some(&a));
assert_eq!(
r.durable_pos, 480,
"post-seed reconnect should produce a coherent gate position equal to the slower node, not freeze at 0"
);
}
#[test]
fn race_b1_pre_seed_freeze_is_what_the_fix_avoids() {
let p = parse("persisted>=2").unwrap();
let m = metrics((0, 0), (999, 999));
let a = flags(true, false);
let r = evaluate_durability(&p, WireSeq::new(500), Some(&m), Some(&a));
assert_eq!(
r.durable_pos, 0,
"the gate behaviour under (active=true, cursor=0) — if the senders ever fail to seed before flipping active, this is the freeze the operator would see"
);
}
#[test]
fn race_b2_disconnect_window_doesnt_open_gate_spuriously() {
let p = parse("persisted>=2").unwrap();
let m = metrics((0, 0), (999, 999));
let a = flags(true, false);
let r = evaluate_durability(&p, WireSeq::new(500), Some(&m), Some(&a));
assert_eq!(r.durable_pos, 0);
let a_disconnected = flags(false, false);
let r_after = evaluate_durability(&p, WireSeq::new(500), Some(&m), Some(&a_disconnected));
assert_eq!(r_after.durable_pos, 0);
assert!(
r_after.degraded,
"post-disconnect view of size 1 cannot meet persisted>=2 → degraded"
);
}
#[test]
fn race_invariant_zero_cursor_never_opens_gate_above_slower_node() {
let p = parse("persisted>=2").unwrap();
let m = metrics((0, 0), (999, 999));
let a = flags(true, false);
for primary_pos in [0, 1, 100, 500, 1_000_000_000_u64] {
let r = evaluate_durability(&p, WireSeq::new(primary_pos), Some(&m), Some(&a));
assert_eq!(
r.durable_pos, 0,
"race-window observation must not open the gate above 0 for any primary position (got {} for primary_pos={primary_pos})",
r.durable_pos
);
}
}
#[cfg(feature = "tick-to-trade")]
#[test]
fn gate_cross_tracker_records_journal_when_strictly_below() {
let mut t = GateCrossTracker::new(10);
t.observe(5, Some(100), 1_000);
t.observe(15, Some(100), 2_000);
assert_eq!(t.journal_crossed(), Some(2_000));
assert_eq!(t.replica_crossed(), None);
}
#[cfg(feature = "tick-to-trade")]
#[test]
fn gate_cross_tracker_records_replica_when_strictly_below() {
let mut t = GateCrossTracker::new(10);
t.observe(50, Some(5), 1_000);
t.observe(50, Some(12), 2_000);
assert_eq!(t.journal_crossed(), None);
assert_eq!(t.replica_crossed(), Some(2_000));
}
#[cfg(feature = "tick-to-trade")]
#[test]
fn gate_cross_tracker_records_both_when_both_below() {
let mut t = GateCrossTracker::new(100);
t.observe(50, Some(60), 1_000); t.observe(105, Some(60), 2_000); t.observe(105, Some(110), 3_000); assert_eq!(t.journal_crossed(), Some(2_000));
assert_eq!(t.replica_crossed(), Some(3_000));
}
#[cfg(feature = "tick-to-trade")]
#[test]
fn gate_cross_tracker_skips_cursor_already_past_at_entry() {
let mut t = GateCrossTracker::new(10);
t.observe(50, Some(100), 1_000);
t.observe(60, Some(110), 2_000);
assert_eq!(t.journal_crossed(), None);
assert_eq!(t.replica_crossed(), None);
}
#[cfg(feature = "tick-to-trade")]
#[test]
fn gate_cross_tracker_first_observation_only_for_cross_decision() {
let mut t = GateCrossTracker::new(10);
t.observe(50, Some(5), 1_000); t.observe(20, Some(12), 2_000); assert_eq!(t.journal_crossed(), None);
assert_eq!(t.replica_crossed(), Some(2_000));
}
#[cfg(feature = "tick-to-trade")]
#[test]
fn gate_cross_tracker_holds_first_cross_only() {
let mut t = GateCrossTracker::new(10);
t.observe(5, Some(100), 1_000);
t.observe(15, Some(100), 2_000); t.observe(25, Some(100), 3_000); assert_eq!(t.journal_crossed(), Some(2_000));
}
#[cfg(feature = "tick-to-trade")]
#[test]
fn gate_cross_tracker_ignores_replica_dropping_out_mid_wait() {
let mut t = GateCrossTracker::new(100);
t.observe(50, Some(60), 1_000); t.observe(105, None, 2_000); t.observe(105, None, 3_000); assert_eq!(t.journal_crossed(), Some(2_000));
assert_eq!(t.replica_crossed(), None);
t.observe(105, Some(150), 4_000);
assert_eq!(t.replica_crossed(), Some(4_000));
}
#[cfg(feature = "tick-to-trade")]
#[test]
fn gate_cross_tracker_records_nothing_when_no_replica_clause() {
let mut t = GateCrossTracker::new(100);
t.observe(50, None, 1_000);
t.observe(105, None, 2_000);
assert_eq!(t.journal_crossed(), Some(2_000));
assert_eq!(t.replica_crossed(), None);
}
fn logger_test_policy() -> crate::ack_policy::Policy {
parse("persisted>=2").unwrap()
}
fn drive_logger(
logger: &mut DegradationLogger,
utilization: &StageUtilization,
policy: &crate::ack_policy::Policy,
start: Instant,
states: &[bool],
step: Duration,
) -> bool {
for (i, &state) in states.iter().enumerate() {
let now = start + step.checked_mul(i as u32).unwrap_or(Duration::ZERO);
logger.tick(policy, utilization, state, now, Duration::from_secs(5));
}
utilization.policy_degraded.load(Ordering::Relaxed)
}
#[test]
fn logger_gauge_tracks_state_immediately() {
let p = logger_test_policy();
let utilization = StageUtilization::new();
let now = Instant::now();
let mut logger = DegradationLogger::new(now);
logger.tick(&p, &utilization, true, now, Duration::from_secs(5));
assert!(utilization.policy_degraded.load(Ordering::Relaxed));
logger.tick(
&p,
&utilization,
false,
now + Duration::from_millis(50),
Duration::from_secs(5),
);
assert!(!utilization.policy_degraded.load(Ordering::Relaxed));
}
#[test]
fn logger_starting_degraded_marks_initial_state_logged() {
let p = logger_test_policy();
let utilization = StageUtilization::new();
let now = Instant::now();
let mut logger = DegradationLogger::new_starting_degraded(now, &p);
logger.tick(
&p,
&utilization,
true,
now + Duration::from_millis(10),
Duration::from_secs(5),
);
assert!(utilization.policy_degraded.load(Ordering::Relaxed));
}
#[test]
fn logger_accumulates_degraded_seconds_only_while_degraded() {
let p = logger_test_policy();
let utilization = StageUtilization::new();
let start = Instant::now();
let mut logger = DegradationLogger::new(start);
let sec = Duration::from_secs(1);
logger.tick(&p, &utilization, true, start, Duration::from_secs(5));
assert_eq!(utilization.policy_degraded_nanos.load(Ordering::Relaxed), 0);
logger.tick(&p, &utilization, true, start + sec, Duration::from_secs(5));
logger.tick(
&p,
&utilization,
false,
start + 2 * sec,
Duration::from_secs(5),
);
assert_eq!(
utilization.policy_degraded_nanos.load(Ordering::Relaxed),
2 * sec.as_nanos() as u64
);
logger.tick(
&p,
&utilization,
false,
start + 3 * sec,
Duration::from_secs(5),
);
assert_eq!(
utilization.policy_degraded_nanos.load(Ordering::Relaxed),
2 * sec.as_nanos() as u64
);
}
#[test]
fn logger_starting_degraded_accrues_from_construction() {
let p = logger_test_policy();
let utilization = StageUtilization::new();
let start = Instant::now();
let mut logger = DegradationLogger::new_starting_degraded(start, &p);
logger.tick(
&p,
&utilization,
true,
start + Duration::from_secs(1),
Duration::from_secs(5),
);
assert_eq!(
utilization.policy_degraded_nanos.load(Ordering::Relaxed),
Duration::from_secs(1).as_nanos() as u64
);
}
#[test]
fn logger_reseed_flushes_in_flight_degraded_accrual() {
let p = logger_test_policy();
let utilization = StageUtilization::new();
let start = Instant::now();
let sec = Duration::from_secs(1);
let mut logger = DegradationLogger::new_starting_degraded(start, &p);
logger.tick(&p, &utilization, true, start, Duration::from_secs(5));
assert_eq!(utilization.policy_degraded_nanos.load(Ordering::Relaxed), 0);
logger.reseed(&utilization, start + 30 * sec);
assert_eq!(
utilization.policy_degraded_nanos.load(Ordering::Relaxed),
30 * sec.as_nanos() as u64
);
logger.tick(
&p,
&utilization,
false,
start + 31 * sec,
Duration::from_secs(5),
);
assert_eq!(
utilization.policy_degraded_nanos.load(Ordering::Relaxed),
30 * sec.as_nanos() as u64
);
}
#[test]
fn logger_starting_degraded_then_first_tick_healthy_counts_construction_interval() {
let p = logger_test_policy();
let utilization = StageUtilization::new();
let start = Instant::now();
let mut logger = DegradationLogger::new_starting_degraded(start, &p);
logger.tick(
&p,
&utilization,
false,
start + Duration::from_secs(2),
Duration::from_secs(5),
);
assert_eq!(
utilization.policy_degraded_nanos.load(Ordering::Relaxed),
Duration::from_secs(2).as_nanos() as u64
);
}
#[test]
fn logger_counter_matches_total_degraded_time_across_flaps() {
let p = logger_test_policy();
let utilization = StageUtilization::new();
let start = Instant::now();
let step = Duration::from_millis(500);
let mut logger = DegradationLogger::new(start);
let states = [false, true, true, false, true, false];
for (i, &s) in states.iter().enumerate() {
logger.tick(
&p,
&utilization,
s,
start + step * i as u32,
Duration::from_secs(5),
);
}
assert_eq!(
utilization.policy_degraded_nanos.load(Ordering::Relaxed),
(3 * step).as_nanos() as u64
);
}
#[test]
fn logger_handles_rapid_flap_without_panic() {
let p = logger_test_policy();
let utilization = StageUtilization::new();
let now = Instant::now();
let mut logger = DegradationLogger::new(now);
let states: Vec<bool> = (0..100u32).map(|i| i.is_multiple_of(2)).collect();
let final_state = drive_logger(
&mut logger,
&utilization,
&p,
now,
&states,
Duration::from_millis(100),
);
assert!(!final_state);
}
#[test]
fn logger_sustained_degraded_eventually_settles() {
let p = logger_test_policy();
let utilization = StageUtilization::new();
let now = Instant::now();
let mut logger = DegradationLogger::new(now);
let final_state = drive_logger(
&mut logger,
&utilization,
&p,
now,
&[true; 5],
Duration::from_millis(500),
);
assert!(final_state);
}
#[test]
fn logger_recovery_to_healthy_settles_gauge() {
let p = logger_test_policy();
let utilization = StageUtilization::new();
let now = Instant::now();
let mut logger = DegradationLogger::new_starting_degraded(now, &p);
let mut states = vec![true; 5]; states.extend(vec![false; 5]); let final_state = drive_logger(
&mut logger,
&utilization,
&p,
now,
&states,
Duration::from_millis(500),
);
assert!(!final_state);
}
mod dirty_tracking {
use super::super::{mark_dirty, unmark_dirty};
use super::flush_sends::entry_for;
use std::os::unix::net::UnixStream;
fn entry() -> super::super::ConnectionEntry {
let (tx, _rx) = UnixStream::pair().expect("socketpair");
let mut e = entry_for(tx, b"payload");
e.dirty = false;
e
}
#[test]
fn repeat_appends_enqueue_a_connection_once() {
let mut e = entry();
let mut dirty = Vec::new();
for _ in 0..8 {
mark_dirty(&mut e, 42, &mut dirty);
}
assert_eq!(dirty, vec![42], "the flag is the dedup");
assert!(e.dirty);
}
#[test]
fn teardown_takes_a_connection_off_the_list() {
let mut a = entry();
let mut b = entry();
let mut dirty = Vec::new();
mark_dirty(&mut a, 1, &mut dirty);
mark_dirty(&mut b, 2, &mut dirty);
unmark_dirty(1, &mut dirty);
assert_eq!(dirty, vec![2], "order of the survivors is preserved");
}
}
mod append_frames {
use super::super::{AppendOutcome, ConnectionEntry, MAX_SEND_BUF, append_frames};
use super::flush_sends::entry_for;
use std::os::unix::net::UnixStream;
use std::time::Instant;
const TRAILER: &[u8] = &[0xEE, 0xEE];
fn entry(prefill: usize) -> ConnectionEntry {
let (tx, _rx) = UnixStream::pair().expect("socketpair");
let mut e = entry_for(tx, &vec![0u8; prefill]);
e.dirty = false;
e
}
#[test]
fn payload_and_terminator_land_in_one_append() {
let mut e = entry(0);
let mut dirty = Vec::new();
let mut to_remove = Vec::new();
let encoded = [1u8, 2, 3, 4, 0xFF];
let outcome = append_frames(
Some(Ok(4)),
TRAILER,
7,
&mut e,
&encoded,
Instant::now(),
&mut dirty,
&mut to_remove,
);
assert!(matches!(outcome, AppendOutcome::Continue));
assert_eq!(
e.send_buf,
vec![1, 2, 3, 4, 0xEE, 0xEE],
"payload then terminator, nothing past the encoder's length"
);
assert_eq!(dirty, vec![7], "one dirty marking for the pair");
assert!(to_remove.is_empty());
}
#[test]
fn a_slot_with_no_bytes_does_not_dirty_the_connection() {
let mut e = entry(0);
let mut dirty = Vec::new();
let mut to_remove = Vec::new();
let outcome = append_frames(
None,
&[],
7,
&mut e,
&[],
Instant::now(),
&mut dirty,
&mut to_remove,
);
assert!(matches!(outcome, AppendOutcome::Continue));
assert!(e.send_buf.is_empty());
assert!(dirty.is_empty());
assert!(!e.dirty);
}
#[test]
fn the_cap_is_checked_against_both_frames_together() {
let mut e = entry(MAX_SEND_BUF - 5);
let mut dirty = Vec::new();
let mut to_remove = Vec::new();
let encoded = [1u8, 2, 3, 4];
let outcome = append_frames(
Some(Ok(4)),
TRAILER,
7,
&mut e,
&encoded,
Instant::now(),
&mut dirty,
&mut to_remove,
);
assert!(matches!(outcome, AppendOutcome::ConnectionDropped));
assert_eq!(to_remove, vec![7]);
assert_eq!(e.send_buf.len(), MAX_SEND_BUF - 5, "no partial append");
assert!(dirty.is_empty(), "a dropped connection is not queued");
}
#[test]
fn an_encode_failure_still_terminates_the_request() {
let mut e = entry(0);
let mut dirty = Vec::new();
let mut to_remove = Vec::new();
let outcome = append_frames(
Some(Err("encode error")),
TRAILER,
7,
&mut e,
&[],
Instant::now(),
&mut dirty,
&mut to_remove,
);
assert!(matches!(outcome, AppendOutcome::Continue));
assert_eq!(e.send_buf, TRAILER);
assert_eq!(dirty, vec![7]);
}
}
mod flush_cadence {
use super::super::{FLUSH_BYTES_THRESHOLD, FLUSH_SLOT_INTERVAL, FlushCadence};
#[test]
fn byte_threshold_fires_on_a_single_fat_connection() {
let mut c = FlushCadence::default();
c.on_append(FLUSH_BYTES_THRESHOLD - 1);
assert!(!c.is_due());
c.on_append(FLUSH_BYTES_THRESHOLD);
assert!(c.is_due());
}
#[test]
fn slot_interval_bounds_the_age_of_tiny_buffers() {
let mut c = FlushCadence::default();
for _ in 0..FLUSH_SLOT_INTERVAL - 1 {
c.on_append(40);
assert!(!c.is_due());
}
c.on_append(40);
assert!(
c.is_due(),
"age bound must fire on the {FLUSH_SLOT_INTERVAL}th slot"
);
}
#[test]
fn flushing_restarts_both_triggers() {
let mut c = FlushCadence::default();
for _ in 0..FLUSH_SLOT_INTERVAL {
c.on_append(FLUSH_BYTES_THRESHOLD);
}
assert!(c.is_due());
c.on_flush();
assert!(
!c.is_due(),
"a flush restarts the byte flag and the counter"
);
for _ in 0..FLUSH_SLOT_INTERVAL - 1 {
c.on_append(1);
}
assert!(!c.is_due());
c.on_append(1);
assert!(c.is_due());
}
}
mod heartbeat {
use super::super::{MAX_SEND_BUF, heartbeat_due};
use super::flush_sends::entry_for;
use std::os::unix::net::UnixStream;
use std::time::{Duration, Instant};
const INTERVAL: Duration = Duration::from_secs(1);
const FRAME_LEN: usize = 13;
fn idle_entry(payload: &[u8]) -> Option<super::super::ConnectionEntry> {
let (tx, _rx) = UnixStream::pair().unwrap();
let mut entry = entry_for(tx, payload);
entry.last_send = Instant::now().checked_sub(INTERVAL * 2)?;
Some(entry)
}
#[test]
fn idle_unblocked_connection_gets_a_heartbeat() {
let Some(entry) = idle_entry(&[]) else {
eprintln!("monotonic clock too young to backdate; skipping");
return;
};
assert!(heartbeat_due(&entry, Instant::now(), INTERVAL, FRAME_LEN));
}
#[test]
fn recently_active_connection_is_skipped() {
let (tx, _rx) = UnixStream::pair().unwrap();
let entry = entry_for(tx, &[]);
assert!(
!heartbeat_due(&entry, Instant::now(), INTERVAL, FRAME_LEN),
"a connection inside the interval must not be pinged"
);
}
#[test]
fn blocked_connection_is_skipped() {
let Some(mut entry) = idle_entry(&[]) else {
eprintln!("monotonic clock too young to backdate; skipping");
return;
};
entry.blocked_since = Some(Instant::now());
assert!(!heartbeat_due(&entry, Instant::now(), INTERVAL, FRAME_LEN));
}
#[test]
fn append_never_exceeds_the_send_buffer_cap() {
let payload = vec![0u8; MAX_SEND_BUF - FRAME_LEN + 1];
let Some(entry) = idle_entry(&payload) else {
eprintln!("monotonic clock too young to backdate; skipping");
return;
};
assert!(!heartbeat_due(&entry, Instant::now(), INTERVAL, FRAME_LEN));
let payload = vec![0u8; MAX_SEND_BUF - FRAME_LEN];
let Some(entry) = idle_entry(&payload) else {
eprintln!("monotonic clock too young to backdate; skipping");
return;
};
assert!(
heartbeat_due(&entry, Instant::now(), INTERVAL, FRAME_LEN),
"a frame that exactly fits is allowed"
);
}
}
mod flush_sends {
use super::super::{
BLOCKED_RETRY_INTERVAL, BLOCKED_SEND_TIMEOUT, ConnectionEntry, flush_sends,
};
use io_uring::IoUring;
use rustc_hash::FxHashMap;
use std::io::Read as _;
use std::os::unix::io::{AsRawFd, RawFd};
use std::os::unix::net::UnixStream;
use std::time::{Duration, Instant};
pub(super) fn entry_for(stream: UnixStream, payload: &[u8]) -> ConnectionEntry {
ConnectionEntry {
fd: stream.as_raw_fd(),
_owner: Box::new(stream),
send_buf: payload.to_vec(),
last_send: Instant::now(),
blocked_since: None,
last_send_attempt: Instant::now(),
dirty: true,
}
}
pub(super) fn fill_socket(fd: RawFd) {
let junk = [0u8; 4096];
loop {
let n =
unsafe { libc::send(fd, junk.as_ptr().cast(), junk.len(), libc::MSG_DONTWAIT) };
if n < 0 {
let err = std::io::Error::last_os_error();
assert_eq!(
err.raw_os_error(),
Some(libc::EAGAIN),
"unexpected errno while filling socket"
);
break;
}
}
}
#[test]
fn slow_peer_does_not_block_flush_and_healthy_peer_is_delivered() {
let mut ring = IoUring::new(64).unwrap();
let (slow_tx, _slow_rx) = UnixStream::pair().unwrap();
let (fast_tx, mut fast_rx) = UnixStream::pair().unwrap();
fill_socket(slow_tx.as_raw_fd());
let mut connections = FxHashMap::default();
connections.insert(1u64, entry_for(slow_tx, &[0xAA; 1024]));
connections.insert(2u64, entry_for(fast_tx, b"hello"));
let mut dirty: Vec<u64> = vec![1, 2];
let mut to_remove = Vec::new();
let mut cqes = Vec::new();
let start = Instant::now();
flush_sends(
&mut ring,
&mut connections,
&mut dirty,
&mut to_remove,
&mut cqes,
);
assert!(
start.elapsed() < Duration::from_secs(1),
"flush blocked on a slow peer"
);
assert!(to_remove.is_empty(), "nobody dropped on first refusal");
let slow = &connections[&1];
assert!(
!slow.send_buf.is_empty(),
"slow peer keeps its undelivered bytes"
);
assert!(slow.blocked_since.is_some(), "slow peer marked blocked");
assert!(dirty.contains(&1), "slow peer stays dirty for retry");
assert!(slow.dirty, "and its flag agrees with the list");
assert!(connections[&2].send_buf.is_empty(), "healthy peer drained");
assert!(!dirty.contains(&2), "healthy peer leaves the dirty set");
assert!(!connections[&2].dirty, "and its flag clears with it");
fast_rx
.set_read_timeout(Some(Duration::from_secs(1)))
.unwrap();
let mut buf = [0u8; 5];
fast_rx.read_exact(&mut buf).unwrap();
assert_eq!(&buf, b"hello", "healthy peer's bytes reached the socket");
}
#[test]
fn partial_send_keeps_remainder_and_marks_blocked() {
let mut ring = IoUring::new(64).unwrap();
let (tx, _rx) = UnixStream::pair().unwrap();
let sz: libc::c_int = 8192;
let rc = unsafe {
libc::setsockopt(
tx.as_raw_fd(),
libc::SOL_SOCKET,
libc::SO_SNDBUF,
(&raw const sz).cast(),
size_of::<libc::c_int>() as libc::socklen_t,
)
};
assert_eq!(rc, 0, "setsockopt(SO_SNDBUF) failed");
let payload = vec![0x55u8; 512 * 1024];
let Some(stale_block) = Instant::now().checked_sub(BLOCKED_SEND_TIMEOUT / 2) else {
eprintln!("monotonic clock too young to backdate; skipping");
return;
};
let mut entry = entry_for(tx, &payload);
entry.blocked_since = Some(stale_block);
entry.last_send_attempt = stale_block;
let mut connections = FxHashMap::default();
connections.insert(1u64, entry);
let mut dirty: Vec<u64> = vec![1];
let mut to_remove = Vec::new();
let mut cqes = Vec::new();
flush_sends(
&mut ring,
&mut connections,
&mut dirty,
&mut to_remove,
&mut cqes,
);
let entry = &connections[&1];
assert!(entry.send_buf.len() < payload.len(), "some bytes were sent");
assert!(!entry.send_buf.is_empty(), "remainder kept for retry");
let refreshed = entry.blocked_since.expect("partial send marks blocked");
assert!(
refreshed > stale_block,
"forward progress restarts the blocked clock"
);
assert!(dirty.contains(&1));
assert!(to_remove.is_empty());
}
#[test]
fn blocked_past_timeout_is_dropped() {
let mut ring = IoUring::new(64).unwrap();
let (tx, _rx) = UnixStream::pair().unwrap();
fill_socket(tx.as_raw_fd());
let mut entry = entry_for(tx, &[1u8; 64]);
let Some(expired) = Instant::now().checked_sub(BLOCKED_SEND_TIMEOUT) else {
eprintln!("monotonic clock too young to backdate; skipping");
return;
};
entry.blocked_since = Some(expired);
let mut connections = FxHashMap::default();
connections.insert(7u64, entry);
let mut dirty: Vec<u64> = vec![7];
let mut to_remove = Vec::new();
let mut cqes = Vec::new();
flush_sends(
&mut ring,
&mut connections,
&mut dirty,
&mut to_remove,
&mut cqes,
);
assert_eq!(to_remove, vec![7], "timed-out peer queued for removal");
assert!(!dirty.contains(&7), "dropped peer leaves the dirty set");
}
#[test]
fn blocked_retry_is_paced() {
let mut ring = IoUring::new(64).unwrap();
let (tx, _rx) = UnixStream::pair().unwrap();
fill_socket(tx.as_raw_fd());
let mut entry = entry_for(tx, &[1u8; 64]);
entry.blocked_since = Some(Instant::now());
entry.last_send_attempt = Instant::now() + Duration::from_secs(1);
let attempted_at = entry.last_send_attempt;
let mut connections = FxHashMap::default();
connections.insert(3u64, entry);
let mut dirty: Vec<u64> = vec![3];
let mut to_remove = Vec::new();
let mut cqes = Vec::new();
flush_sends(
&mut ring,
&mut connections,
&mut dirty,
&mut to_remove,
&mut cqes,
);
assert_eq!(
connections[&3].last_send_attempt, attempted_at,
"no SEND submitted inside the pacing window"
);
assert!(dirty.contains(&3), "paced peer stays dirty");
assert!(to_remove.is_empty());
let Some(backdated) = Instant::now().checked_sub(BLOCKED_RETRY_INTERVAL * 2) else {
eprintln!("monotonic clock too young to backdate; skipping");
return;
};
connections.get_mut(&3).unwrap().last_send_attempt = backdated;
flush_sends(
&mut ring,
&mut connections,
&mut dirty,
&mut to_remove,
&mut cqes,
);
assert!(
connections[&3].last_send_attempt > backdated,
"retry submitted once the pacing window elapsed"
);
}
#[test]
fn teardown_shuts_the_socket_despite_other_dups_and_releases_the_permit() {
use std::sync::atomic::{AtomicU64, Ordering};
let (tx, mut peer) = UnixStream::pair().unwrap();
let reader_dup = tx.try_clone().unwrap();
let entry = entry_for(tx, &[]);
let active = AtomicU64::new(1);
super::super::teardown_dropped(entry, &active);
assert_eq!(active.load(Ordering::Relaxed), 0, "permit released");
peer.set_read_timeout(Some(Duration::from_secs(1))).unwrap();
let mut buf = [0u8; 1];
assert_eq!(
peer.read(&mut buf).expect("EOF, not a timeout"),
0,
"peer observes EOF even though another dup is still open"
);
drop(reader_dup);
}
}
mod early_drain {
use std::os::unix::io::AsRawFd;
use std::os::unix::net::UnixStream;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::thread;
use std::time::{Duration, Instant};
use counter_server::{CounterReport, ResponseEncoder};
use io_uring::IoUring;
use melin_wire_protocol::control::RefusalKind;
use super::early_release::widest_reason;
use super::flush_sends::{entry_for, fill_socket};
use super::refusal_order::{Seen, early, early_id, frames_of, read_frame};
use crate::batch_boundary::BatchBoundary;
use crate::early_reply::{EARLY_REPLY_QUEUE_CAPACITY, EarlyKind, early_reply_channel};
use crate::response::{Egress, MAX_SEND_BUF};
use crate::response_frame::{EarlyReplyBuf, MAX_EARLY_REPLY};
const BURST: u64 = 800;
fn kind(n: u64) -> EarlyKind<CounterReport> {
if n.is_multiple_of(2) {
EarlyKind::Busy
} else {
EarlyKind::Refused {
kind: RefusalKind::PermissionDenied,
reason: widest_reason(),
}
}
}
fn egress_over(stream: UnixStream) -> (Egress, Arc<AtomicU64>) {
let active = Arc::new(AtomicU64::new(1));
let mut egress = Egress::new(
IoUring::new(64).expect("an io_uring"),
64,
Arc::clone(&active),
#[cfg(feature = "latency-trace")]
melin_transport_core::trace::register_stage("test: server e2e"),
#[cfg(feature = "tick-to-trade")]
melin_transport_core::trace::register_stage("test: egress"),
);
let mut entry = entry_for(stream, &[]);
entry.dirty = false;
egress.connections.insert(1, entry);
(egress, active)
}
fn drain_burst(egress: &mut Egress) -> usize {
assert!(BURST as usize <= EARLY_REPLY_QUEUE_CAPACITY);
let (mut tx, mut rx) =
early_reply_channel(crate::early_reply::EARLY_REPLY_QUEUE_CAPACITY);
for n in 0..BURST {
tx.try_send(early(1, n, kind(n))).expect("queue has room");
}
tx.flush();
let boundary = BatchBoundary::new();
let at = boundary.at_boundary().expect("between events");
let mut early_buf: EarlyReplyBuf = [0u8; MAX_EARLY_REPLY];
let released = egress.release_early_replies(
&mut rx.register(|| {}),
&at,
&ResponseEncoder,
&mut early_buf,
Instant::now,
);
egress.drop_overflowed();
egress.flush();
released
}
#[test]
fn a_drain_past_the_send_buffer_flushes_as_it_goes_and_keeps_a_reading_client() {
let expected: Vec<Seen> = (0..BURST)
.flat_map(|n| frames_of(kind(n), early_id(n)))
.collect();
let wire_bytes: usize = (0..BURST)
.map(|n| match kind(n) {
EarlyKind::Busy => 13,
_ => 15 + widest_reason().len(),
})
.sum();
assert!(wire_bytes > MAX_SEND_BUF, "the burst outgrows the buffer");
let (server, mut client) = UnixStream::pair().expect("socketpair");
client
.set_read_timeout(Some(Duration::from_secs(5)))
.expect("set read timeout");
let count = expected.len();
let reader = thread::spawn(move || {
(0..count)
.map_while(|_| read_frame(&mut client).ok())
.collect::<Vec<_>>()
});
let (mut egress, active) = egress_over(server);
assert_eq!(drain_burst(&mut egress), BURST as usize);
let deadline = Instant::now() + Duration::from_secs(5);
while !egress.dirty.is_empty() && Instant::now() < deadline {
thread::sleep(Duration::from_millis(1));
egress.flush();
}
assert!(
egress.connections.contains_key(&1),
"a client reading its replies was dropped"
);
assert_eq!(active.load(Ordering::Relaxed), 1);
assert_eq!(reader.join().expect("reader"), expected, "in queue order");
}
#[test]
fn a_drain_to_a_client_that_does_not_read_drops_it_at_the_cap() {
let (server, _client) = UnixStream::pair().expect("socketpair");
fill_socket(server.as_raw_fd());
let (mut egress, active) = egress_over(server);
assert_eq!(drain_burst(&mut egress), BURST as usize);
assert!(
!egress.connections.contains_key(&1),
"a client that reads nothing kept its connection"
);
assert!(!egress.dirty.contains(&1));
assert_eq!(
active.load(Ordering::Relaxed),
0,
"its permit released once"
);
}
#[test]
fn a_drain_to_a_client_that_has_hung_up_tears_it_down_once() {
let (server, client) = UnixStream::pair().expect("socketpair");
drop(client);
let (mut egress, active) = egress_over(server);
assert_eq!(drain_burst(&mut egress), BURST as usize);
assert!(!egress.connections.contains_key(&1));
assert!(egress.to_remove.is_empty(), "nothing left to tear down");
assert_eq!(
active.load(Ordering::Relaxed),
0,
"its permit released once"
);
}
#[test]
fn a_due_cadence_with_nothing_dirty_is_restarted() {
let (server, _client) = UnixStream::pair().expect("socketpair");
let (mut egress, _active) = egress_over(server);
for _ in 0..super::super::FLUSH_SLOT_INTERVAL {
egress.cadence.on_append(1);
}
assert!(egress.cadence.is_due());
assert!(egress.dirty.is_empty());
egress.flush_if_due();
assert!(!egress.cadence.is_due());
}
}
mod gate_wait_hook {
use std::os::unix::net::UnixStream;
use std::sync::Arc;
use std::sync::atomic::AtomicU64;
use std::sync::mpsc;
use std::time::Duration;
use counter_server::{CounterReport, ResponseEncoder};
use io_uring::IoUring;
use super::refusal_order::{Seen, connection, early, early_id, frames_of, read_frame};
use crate::batch_boundary::BatchBoundary;
use crate::durability_gate::GateWait;
use crate::early_reply::{EarlyKind, early_reply_channel};
use crate::response::{Egress, GateWaitHook};
use crate::response_frame::{EarlyReplyBuf, MAX_EARLY_REPLY};
const BUSY: EarlyKind<CounterReport> = EarlyKind::Busy;
fn one_turn(between_events: bool) -> (Egress, UnixStream) {
let mut egress = Egress::new(
IoUring::new(64).expect("an io_uring"),
64,
Arc::new(AtomicU64::new(1)),
#[cfg(feature = "latency-trace")]
melin_transport_core::trace::register_stage("test: server e2e"),
#[cfg(feature = "tick-to-trade")]
melin_transport_core::trace::register_stage("test: egress"),
);
let (control_tx, control_rx) = mpsc::channel();
let (mut tx, mut rx) =
early_reply_channel(crate::early_reply::EARLY_REPLY_QUEUE_CAPACITY);
let mut registered = rx.register(|| {});
let (event, client) = connection(1);
control_tx.send(event).expect("the receiver is held");
tx.try_send(early(1, 0, BUSY)).expect("queue has room");
tx.flush();
let boundary = BatchBoundary::new();
let mut early_buf: EarlyReplyBuf = [0u8; MAX_EARLY_REPLY];
GateWaitHook {
egress: &mut egress,
at_boundary: if between_events {
Some(boundary.at_boundary().expect("between events"))
} else {
None
},
registered: &mut registered,
control_rx: &control_rx,
encoder: &ResponseEncoder,
early_buf: &mut early_buf,
}
.while_waiting();
(egress, client)
}
#[test]
fn between_events_it_registers_sends_and_flushes() {
let (egress, mut client) = one_turn(true);
assert!(egress.connections.contains_key(&1), "not registered");
assert!(egress.dirty.is_empty(), "sent but left buffered");
client
.set_read_timeout(Some(Duration::from_secs(5)))
.expect("set read timeout");
let expected = frames_of(BUSY, early_id(0));
let seen: Vec<Seen> = expected
.iter()
.map(|_| read_frame(&mut client).expect("the busy's frames"))
.collect();
assert_eq!(seen, expected);
}
#[test]
fn inside_an_event_it_does_nothing() {
let (egress, mut client) = one_turn(false);
assert!(egress.connections.is_empty(), "control drained");
assert!(egress.dirty.is_empty());
client
.set_read_timeout(Some(Duration::from_millis(50)))
.expect("set read timeout");
assert!(read_frame(&mut client).is_err(), "a reply went out");
}
}
mod registration_race {
use std::io::{Read, Write};
use std::os::unix::io::AsRawFd;
use std::os::unix::net::UnixStream;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, mpsc};
use std::thread;
use std::time::Duration;
use counter_server::{
Counter, CounterQuery, CounterReport, KIND_RESP_ACK, ResponseEncoder,
};
use melin_pipeline::ring::DisruptorBuilder;
use melin_pipeline::wait::WaitStrategy;
use melin_transport_core::fence::FenceState;
use melin_transport_core::pipeline::{OutputPayload, OutputSlot, StageUtilization};
use melin_transport_core::{DurableWireSeqCursor, WireSeq};
use melin_wire_protocol::blocking::BlockingFrameWriter;
use melin_wire_protocol::control_codec::TAG_APP;
use crate::ControlEvent;
use crate::ack_policy::AckPolicy;
use crate::response::{Response, run};
const FRAME_LEN: usize = 14;
const READ_TIMEOUT: Duration = Duration::from_secs(5);
#[test]
fn a_reply_that_lands_with_its_connection_is_delivered() {
let (mut producer, mut consumers) =
DisruptorBuilder::<OutputSlot<CounterReport, CounterQuery>>::new(64)
.add_consumer()
.build(WaitStrategy::SpinThenYield);
let consumer = consumers.pop().expect("one consumer was requested");
let (server_sock, mut client_sock) = UnixStream::pair().expect("socketpair");
let server_fd = server_sock.as_raw_fd();
let writer = BlockingFrameWriter::new(Box::new(server_sock) as Box<dyn Write + Send>);
client_sock
.set_read_timeout(Some(READ_TIMEOUT))
.expect("set read timeout");
let (parked_tx, parked_rx) = mpsc::channel::<()>();
let (resume_tx, resume_rx) = mpsc::channel::<()>();
let mut held = false;
let pause = move || {
if !held {
held = true;
parked_tx.send(()).expect("the test is waiting");
resume_rx.recv().expect("the test resumes the stage");
}
};
let (control_tx, control_rx) = mpsc::channel();
let shutdown = AtomicBool::new(false);
let config = Response::<Counter> {
journal_persisted_wire_seq: DurableWireSeqCursor::detached(WireSeq::new(1)),
policy: crate::response::GatePolicy {
start: crate::durability_gate::GateStart::settled(AckPolicy::Disk),
forced: melin_transport_core::lineage::forced_policy_cell().1,
},
replication_metrics: None,
replica_active: None,
heartbeat_interval: None,
wait: WaitStrategy::SpinThenYield,
utilization: Arc::new(StageUtilization::default()),
encoder: Arc::new(ResponseEncoder),
fence_state: Arc::new(FenceState::new(0)),
active_connections: Arc::new(AtomicU64::new(0)),
early_replies: crate::early_reply::early_reply_channel(
crate::early_reply::EARLY_REPLY_QUEUE_CAPACITY,
)
.1,
ring_sizing: crate::connection_limit::RingSizing::for_max_connections(1)
.expect("a supported cap"),
ready: None,
degraded_release: None,
pause_after_control_drain: Some(Box::new(pause)),
};
thread::scope(|scope| {
let stage = scope.spawn(|| run::<Counter>(consumer, control_rx, config, &shutdown));
parked_rx.recv().expect("the stage reaches the seam");
control_tx
.send(ControlEvent::Connected {
connection_id: 1,
fd: server_fd,
writer,
})
.expect("stage is running");
producer.publish(OutputSlot {
connection_id: 1,
wire_seq: 1,
payload: OutputPayload::Report(CounterReport::Ack { new_value: 7 }),
is_last_in_request: false,
..Default::default()
});
resume_tx.send(()).expect("the stage is parked");
let mut frame = [0u8; FRAME_LEN];
let delivered = client_sock.read_exact(&mut frame);
shutdown.store(true, Ordering::Relaxed);
stage.join().expect("stage panicked");
delivered.expect(
"the reply was dropped: the control channel was drained before the ring was read",
);
assert_eq!(
frame[4..6],
[TAG_APP, KIND_RESP_ACK],
"an application frame carrying the counter's ack"
);
assert_eq!(
u64::from_le_bytes(frame[6..].try_into().expect("8 bytes")),
7,
"the ack carries the counter's value"
);
});
}
}
pub(crate) mod refusal_order {
use std::io::{Read, Write};
use std::os::unix::io::AsRawFd;
use std::os::unix::net::UnixStream;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, mpsc};
use std::thread;
use std::time::Duration;
use counter_server::{
Counter, CounterQuery, CounterReport, KIND_RESP_REJECTED, ResponseEncoder,
};
use melin_pipeline::ring::DisruptorBuilder;
use melin_pipeline::wait::WaitStrategy;
use melin_transport_core::fence::FenceState;
use melin_transport_core::pipeline::{OutputSlot, RequestId, StageUtilization};
use melin_transport_core::{DurableWireSeqCursor, WireSeq};
use melin_wire_protocol::blocking::BlockingFrameWriter;
use melin_wire_protocol::control_codec::{
TAG_APP, TAG_BATCH_END, TAG_REFUSED, TAG_SERVER_BUSY, terminal_request_id,
};
use crate::ControlEvent;
use crate::ack_policy::AckPolicy;
use crate::early_reply::{EarlyKind, EarlyReply, EarlyReplyQueue, early_reply_channel};
use crate::response::{Response, run};
pub(crate) const READ_TIMEOUT: Duration = Duration::from_secs(5);
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum Seen {
App(u8),
Final(u8, RequestId),
Protocol(u8),
}
impl Seen {
pub(crate) fn of(payload: &[u8]) -> Self {
if let Some(id) = terminal_request_id(payload) {
return Seen::Final(payload[0], id);
}
match *payload {
[TAG_APP, kind, ..] => Seen::App(kind),
[TAG_APP] => panic!("a counter response with an empty body"),
[tag, ..] => Seen::Protocol(tag),
[] => panic!("an empty frame on the wire"),
}
}
}
pub(crate) fn early_id(n: u64) -> RequestId {
RequestId(1000 + n)
}
pub(crate) fn early(
connection_id: u64,
n: u64,
kind: EarlyKind<CounterReport>,
) -> EarlyReply<CounterReport> {
EarlyReply {
connection_id,
request_id: early_id(n),
kind,
}
}
pub(crate) fn frames_of(kind: EarlyKind<CounterReport>, id: RequestId) -> Vec<Seen> {
match kind {
EarlyKind::Rejected(_) => {
vec![
Seen::App(KIND_RESP_REJECTED),
Seen::Final(TAG_BATCH_END, id),
]
}
EarlyKind::Filtered => vec![Seen::Final(TAG_BATCH_END, id)],
EarlyKind::Refused { .. } => vec![Seen::Final(TAG_REFUSED, id)],
EarlyKind::Busy => vec![Seen::Final(TAG_SERVER_BUSY, id)],
}
}
pub(super) fn read_frame(sock: &mut UnixStream) -> std::io::Result<Seen> {
let mut len = [0u8; 4];
sock.read_exact(&mut len)?;
let mut payload = vec![0u8; u32::from_le_bytes(len) as usize];
sock.read_exact(&mut payload)?;
Ok(Seen::of(&payload))
}
fn read_into(sock: &mut UnixStream, seen: &mut Vec<Seen>) -> bool {
match read_frame(sock) {
Ok(frame) => {
seen.push(frame);
true
}
Err(_) => false,
}
}
#[test]
fn a_refusal_is_not_released_ahead_of_its_connection() {
let (_producer, mut consumers) =
DisruptorBuilder::<OutputSlot<CounterReport, CounterQuery>>::new(64)
.add_consumer()
.build(WaitStrategy::SpinThenYield);
let consumer = consumers.pop().expect("one consumer was requested");
let (connected_1, mut client_1) = connection(1);
let (connected_2, mut client_2) = connection(2);
let (mut refusals, refusal_rx) =
early_reply_channel(crate::early_reply::EARLY_REPLY_QUEUE_CAPACITY);
let (control_tx, control_rx) = mpsc::channel();
control_tx.send(connected_1).expect("channel open");
let (parked_tx, parked_rx) = mpsc::channel::<()>();
let (resume_tx, resume_rx) = mpsc::channel::<()>();
let mut holds = 0;
let pause = move || {
if holds < 2 {
holds += 1;
parked_tx.send(()).expect("the test is waiting");
resume_rx.recv().expect("the test resumes the stage");
}
};
let shutdown = AtomicBool::new(false);
let config = config(
DurableWireSeqCursor::detached(WireSeq::new(0)),
Arc::new(FenceState::new(0)),
refusal_rx,
Some(Box::new(pause)),
);
thread::scope(|scope| {
let stage = scope.spawn(|| run::<Counter>(consumer, control_rx, config, &shutdown));
parked_rx.recv().expect("the stage reaches the seam");
refusals.try_send(refusal(1, 0)).expect("queue has room");
refusals.flush();
resume_tx.send(()).expect("the stage is parked");
parked_rx.recv().expect("the stage reaches the seam again");
control_tx.send(connected_2).expect("stage is running");
refusals.try_send(refusal(2, 0)).expect("queue has room");
refusals.flush();
resume_tx.send(()).expect("the stage is parked");
let mut seen_1 = Vec::new();
let mut seen_2 = Vec::new();
let got_1 = (0..2).all(|_| read_into(&mut client_1, &mut seen_1));
let got_2 = (0..2).all(|_| read_into(&mut client_2, &mut seen_2));
shutdown.store(true, Ordering::Relaxed);
stage.join().expect("stage panicked");
assert!(got_1, "connection 1's refusal never arrived: {seen_1:x?}");
assert!(
got_2,
"connection 2's refusal was dropped: released before its connection registered"
);
for seen in [seen_1, seen_2] {
assert_eq!(
seen,
[
Seen::App(KIND_RESP_REJECTED),
Seen::Final(TAG_BATCH_END, early_id(0))
]
);
}
});
}
pub(super) fn connection(connection_id: u64) -> (ControlEvent, UnixStream) {
let (server_sock, client_sock) = UnixStream::pair().expect("socketpair");
client_sock
.set_read_timeout(Some(READ_TIMEOUT))
.expect("set read timeout");
let event = ControlEvent::Connected {
connection_id,
fd: server_sock.as_raw_fd(),
writer: BlockingFrameWriter::new(Box::new(server_sock) as Box<dyn Write + Send>),
};
(event, client_sock)
}
pub(crate) fn refusal(connection_id: u64, n: u64) -> EarlyReply<CounterReport> {
early(
connection_id,
n,
EarlyKind::Rejected(CounterReport::Rejected),
)
}
pub(super) fn config(
journal_cursor: DurableWireSeqCursor,
fence_state: Arc<FenceState>,
early_replies: EarlyReplyQueue<CounterReport>,
pause_after_control_drain: Option<Box<dyn FnMut() + Send>>,
) -> Response<Counter> {
Response::<Counter> {
journal_persisted_wire_seq: journal_cursor,
policy: crate::response::GatePolicy {
start: crate::durability_gate::GateStart::settled(AckPolicy::Disk),
forced: melin_transport_core::lineage::forced_policy_cell().1,
},
replication_metrics: None,
replica_active: None,
heartbeat_interval: None,
wait: WaitStrategy::SpinThenYield,
utilization: Arc::new(StageUtilization::default()),
encoder: Arc::new(ResponseEncoder),
fence_state,
active_connections: Arc::new(AtomicU64::new(0)),
early_replies,
ring_sizing: crate::connection_limit::RingSizing::for_max_connections(4)
.expect("a supported cap"),
ready: None,
degraded_release: None,
pause_after_control_drain,
}
}
}
pub(crate) mod early_release {
use std::collections::{BTreeMap, HashMap};
use std::os::unix::net::UnixStream;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, OnceLock, mpsc};
use std::thread::{self, JoinHandle};
use std::time::{Duration, Instant};
use counter_server::{
Counter, CounterQuery, CounterReport, KIND_RESP_ACK, KIND_RESP_REJECTED,
};
use melin_pipeline::ring::{self, DisruptorBuilder};
use melin_pipeline::wait::WaitStrategy;
use melin_transport_core::fence::FenceState;
use melin_transport_core::pipeline::{
OutputPayload, OutputSlot, RequestId, StageUtilization,
};
use melin_transport_core::{DurableWireSeqCursor, WireSeq};
use melin_wire_protocol::control::{MAX_REFUSAL_REASON, RefusalKind};
use melin_wire_protocol::control_codec::TAG_BATCH_END;
use proptest::prelude::*;
use super::refusal_order::{
READ_TIMEOUT, Seen, config, connection, early, early_id, frames_of, read_frame,
};
use crate::ControlEvent;
use crate::ack_policy::AckPolicy;
use crate::early_reply::{
EarlyKind, EarlyReplyQueue, EarlyReplySender, early_reply_channel,
};
use crate::replication::ReplicationMetrics;
use crate::response::run;
pub(crate) const QUIET: Duration = Duration::from_millis(100);
pub(crate) type Slot = OutputSlot<CounterReport, CounterQuery>;
pub(crate) fn every_kind() -> [EarlyKind<CounterReport>; 4] {
[
EarlyKind::Rejected(CounterReport::Rejected),
EarlyKind::Filtered,
EarlyKind::Refused {
kind: RefusalKind::Malformed,
reason: widest_reason(),
},
EarlyKind::Busy,
]
}
pub(crate) fn widest_reason() -> &'static str {
static REASON: OnceLock<&'static str> = OnceLock::new();
REASON.get_or_init(|| "r".repeat(MAX_REFUSAL_REASON).leak())
}
pub(crate) struct StageInputs {
pub(crate) consumer: ring::Consumer<Slot>,
pub(crate) early_replies: EarlyReplyQueue<CounterReport>,
pub(crate) journal: DurableWireSeqCursor,
pub(crate) replication_metrics: Arc<ReplicationMetrics>,
pub(crate) replica_active: [Arc<AtomicBool>; 2],
pub(crate) utilization: Arc<StageUtilization>,
pub(crate) shutdown: Arc<AtomicBool>,
}
pub(crate) trait StageHarness {
fn connect(&mut self, connection_id: u64);
fn next_frame(&mut self, connection_id: u64, within: Duration) -> Option<Seen>;
}
pub(crate) struct Rig<H> {
pub(crate) producer: ring::Producer<Slot>,
pub(crate) early: EarlyReplySender<CounterReport>,
pub(crate) journal: DurableWireSeqCursor,
pub(crate) replica_active: Arc<AtomicBool>,
pub(crate) utilization: Arc<StageUtilization>,
pub(crate) stage: H,
}
pub(crate) fn rig<H: StageHarness>(
start: fn(StageInputs) -> H,
journal_at: u64,
connections: &[u64],
) -> Rig<H> {
let (producer, mut consumers) = DisruptorBuilder::<Slot>::new(128)
.add_consumer()
.build(WaitStrategy::SpinThenYield);
let consumer = consumers.pop().expect("one consumer was requested");
let (early, early_replies) =
early_reply_channel(crate::early_reply::EARLY_REPLY_QUEUE_CAPACITY);
let journal = DurableWireSeqCursor::detached(WireSeq::new(journal_at));
let replication_metrics = Arc::new(ReplicationMetrics::default());
replication_metrics.in_memory_sequence[0].store(1_000_000, Ordering::Relaxed);
let replica_active = Arc::new(AtomicBool::new(true));
let utilization = Arc::new(StageUtilization::default());
let mut stage = start(StageInputs {
consumer,
early_replies,
journal: journal.clone(),
replication_metrics,
replica_active: [
Arc::clone(&replica_active),
Arc::new(AtomicBool::new(false)),
],
utilization: Arc::clone(&utilization),
shutdown: Arc::new(AtomicBool::new(false)),
});
for &id in connections {
stage.connect(id);
}
Rig {
producer,
early,
journal,
replica_active,
utilization,
stage,
}
}
impl<H: StageHarness> Rig<H> {
pub(crate) fn answer_early(
&mut self,
connection_id: u64,
n: u64,
kind: EarlyKind<CounterReport>,
) {
self.early
.try_send(early(connection_id, n, kind))
.expect("queue has room");
self.early.flush();
}
pub(crate) fn publish(
&mut self,
connection_id: u64,
request_id: u64,
wire_seq: u64,
is_last_in_request: bool,
) {
self.producer.publish(Slot {
connection_id,
request_id: RequestId(request_id),
input_seq: wire_seq - 1,
wire_seq,
payload: OutputPayload::Report(CounterReport::Ack {
new_value: wire_seq,
}),
is_last_in_request,
..Default::default()
});
}
pub(crate) fn frames(&mut self, connection_id: u64, count: usize) -> Vec<Seen> {
(0..count)
.map_while(|_| self.stage.next_frame(connection_id, READ_TIMEOUT))
.collect()
}
pub(crate) fn quiet(&mut self, connection_id: u64) -> Option<Seen> {
self.stage.next_frame(connection_id, QUIET)
}
}
fn write_reply(id: u64) -> Vec<Seen> {
vec![
Seen::App(KIND_RESP_ACK),
Seen::Final(TAG_BATCH_END, RequestId(id)),
]
}
pub(crate) fn an_early_reply_waits_out_an_event_split_across_reads<H: StageHarness>(
start: fn(StageInputs) -> H,
) {
let mut rig = rig(start, 10, &[1]);
rig.publish(1, 7, 1, false);
assert_eq!(
rig.frames(1, 1),
[Seen::App(KIND_RESP_ACK)],
"the first slot"
);
rig.answer_early(1, 0, EarlyKind::Busy);
assert_eq!(
rig.quiet(1),
None,
"an early reply inside the event's reply"
);
rig.publish(1, 7, 1, true);
let expected: Vec<Seen> = write_reply(7)
.into_iter()
.chain(frames_of(EarlyKind::Busy, early_id(0)))
.collect();
assert_eq!(rig.frames(1, 3), expected);
}
pub(crate) fn early_replies_overtake_a_held_write<H: StageHarness>(
start: fn(StageInputs) -> H,
kind: EarlyKind<CounterReport>,
) {
let mut rig = rig(start, 0, &[1, 2]);
rig.answer_early(1, 0, kind);
rig.publish(1, 7, 1, true);
let before = frames_of(kind, early_id(0));
assert_eq!(
rig.frames(1, before.len()),
before,
"{kind:?} queued before"
);
rig.answer_early(1, 1, kind);
rig.answer_early(2, 2, kind);
let during = frames_of(kind, early_id(1));
assert_eq!(
rig.frames(1, during.len()),
during,
"{kind:?} during the wait"
);
let other = frames_of(kind, early_id(2));
assert_eq!(
rig.frames(2, other.len()),
other,
"{kind:?} for another connection"
);
assert_eq!(rig.quiet(1), None, "the write is still held");
rig.journal.store(WireSeq::new(1));
assert_eq!(rig.frames(1, 2), write_reply(7), "{kind:?}");
}
pub(crate) fn a_gate_held_inside_an_event_sends_no_early_reply<H: StageHarness>(
start: fn(StageInputs) -> H,
) {
let mut rig = rig(start, 1, &[1]);
rig.publish(1, 7, 1, false);
assert_eq!(
rig.frames(1, 1),
[Seen::App(KIND_RESP_ACK)],
"the first slot"
);
rig.replica_active.store(false, Ordering::Relaxed);
let deadline = Instant::now() + READ_TIMEOUT;
while !rig.utilization.policy_degraded.load(Ordering::Relaxed) {
assert!(Instant::now() < deadline, "the idle re-check never ran");
thread::sleep(Duration::from_millis(5));
}
rig.publish(1, 7, 1, true);
rig.answer_early(1, 0, EarlyKind::Busy);
assert_eq!(rig.quiet(1), None, "an early reply inside the held event");
rig.replica_active.store(true, Ordering::Relaxed);
let expected: Vec<Seen> = write_reply(7)
.into_iter()
.chain(frames_of(EarlyKind::Busy, early_id(0)))
.collect();
assert_eq!(rig.frames(1, 3), expected);
}
pub(crate) fn connection_first_seen_inside_a_gate_wait<H: StageHarness>(
start: fn(StageInputs) -> H,
) {
let mut rig = rig(start, 0, &[1]);
rig.publish(1, 7, 1, true);
rig.answer_early(1, 0, EarlyKind::Busy);
assert_eq!(rig.frames(1, 1), frames_of(EarlyKind::Busy, early_id(0)));
rig.stage.connect(2);
let kind = EarlyKind::Rejected(CounterReport::Rejected);
rig.answer_early(2, 1, kind);
assert_eq!(
rig.frames(2, 2),
[
Seen::App(KIND_RESP_REJECTED),
Seen::Final(TAG_BATCH_END, early_id(1))
],
"dropped: sent before its connection registered"
);
assert_eq!(rig.quiet(1), None, "the write is still held");
rig.journal.store(WireSeq::new(1));
assert_eq!(rig.frames(1, 2), write_reply(7));
}
#[derive(Clone, Debug)]
pub(crate) enum Step {
Event {
connection: u64,
slots: u8,
straddle: bool,
},
Early { connection: u64, kind: u8 },
Open,
}
const SCRIPT_CONNECTIONS: [u64; 3] = [1, 2, 3];
pub(crate) fn step() -> impl Strategy<Value = Step> {
prop_oneof![
(1..=3u64, 1..=3u8, any::<bool>()).prop_map(|(connection, slots, straddle)| {
Step::Event {
connection,
slots,
straddle,
}
}),
(1..=3u64, 0..4u8).prop_map(|(connection, kind)| Step::Early { connection, kind }),
Just(Step::Open),
]
}
pub(crate) fn script() -> impl Strategy<Value = Vec<Step>> {
proptest::collection::vec(step(), 1..24)
}
#[derive(Debug, PartialEq, Eq)]
struct Owed {
responses: usize,
final_tag: u8,
}
pub(crate) fn every_request_is_answered_once_and_whole<H: StageHarness>(
start: fn(StageInputs) -> H,
script: &[Step],
) -> Result<(), TestCaseError> {
let mut rig = rig(start, 0, &SCRIPT_CONNECTIONS);
let mut owed: HashMap<RequestId, Owed> = HashMap::new();
let mut events: BTreeMap<u64, Vec<RequestId>> = BTreeMap::new();
let mut expected_frames: BTreeMap<u64, usize> = BTreeMap::new();
let mut wire_seq = 0;
let mut early_n = 0;
let mut answer = |rig: &mut Rig<H>,
owed: &mut HashMap<RequestId, Owed>,
expected_frames: &mut BTreeMap<u64, usize>,
connection: u64,
kind: EarlyKind<CounterReport>| {
let frames = frames_of(kind, early_id(early_n));
let Some(&Seen::Final(final_tag, _)) = frames.last() else {
unreachable!("every early reply ends in a final frame");
};
owed.insert(
early_id(early_n),
Owed {
responses: frames.len() - 1,
final_tag,
},
);
*expected_frames.entry(connection).or_default() += frames.len();
rig.answer_early(connection, early_n, kind);
early_n += 1;
};
for step in script {
match *step {
Step::Event {
connection,
slots,
straddle,
} => {
wire_seq += 1;
let id = RequestId(wire_seq);
for n in 1..=slots {
rig.publish(connection, id.0, wire_seq, n == slots);
if straddle && n == 1 && slots > 1 {
thread::sleep(Duration::from_millis(2));
answer(
&mut rig,
&mut owed,
&mut expected_frames,
connection,
EarlyKind::Busy,
);
}
}
owed.insert(
id,
Owed {
responses: usize::from(slots),
final_tag: TAG_BATCH_END,
},
);
events.entry(connection).or_default().push(id);
*expected_frames.entry(connection).or_default() += usize::from(slots) + 1;
}
Step::Early { connection, kind } => answer(
&mut rig,
&mut owed,
&mut expected_frames,
connection,
every_kind()[usize::from(kind)],
),
Step::Open => rig.journal.store(WireSeq::new(wire_seq + 1)),
}
}
rig.journal.store(WireSeq::new(wire_seq + 1));
for connection in SCRIPT_CONNECTIONS {
let count = expected_frames.get(&connection).copied().unwrap_or(0);
let frames = rig.frames(connection, count);
prop_assert_eq!(
frames.len(),
count,
"connection {} got {:?}",
connection,
frames
);
prop_assert_eq!(
rig.quiet(connection),
None,
"more than owed on {}",
connection
);
let mut responses = 0;
let mut answered = Vec::new();
for frame in &frames {
match *frame {
Seen::App(_) => responses += 1,
Seen::Final(tag, id) => {
let owed = owed.remove(&id);
prop_assert_eq!(
owed,
Some(Owed {
responses,
final_tag: tag
}),
"request {:?} on {}: {:?}",
id,
connection,
frames
);
answered.push(id);
responses = 0;
}
Seen::Protocol(tag) => {
prop_assert!(false, "an unexpected frame {tag:#x} on {connection}")
}
}
}
prop_assert_eq!(
responses,
0,
"responses with no final frame on {}",
connection
);
let published: Vec<RequestId> =
answered.into_iter().filter(|id| id.0 <= wire_seq).collect();
prop_assert_eq!(
published,
events.remove(&connection).unwrap_or_default(),
"replies to published events out of order on {}",
connection
);
}
prop_assert!(owed.is_empty(), "never answered: {owed:?}");
Ok(())
}
pub(crate) struct UringStage {
control: mpsc::Sender<ControlEvent>,
clients: HashMap<u64, UnixStream>,
shutdown: Arc<AtomicBool>,
handle: Option<JoinHandle<bool>>,
}
pub(crate) fn start_uring(inputs: StageInputs) -> UringStage {
let (control, control_rx) = mpsc::channel();
let mut config = config(
inputs.journal,
Arc::new(FenceState::new(0)),
inputs.early_replies,
None,
);
config.policy.start = crate::durability_gate::GateStart::settled(AckPolicy::DiskAndRam);
config.replication_metrics = Some(inputs.replication_metrics);
config.replica_active = Some(inputs.replica_active);
config.utilization = inputs.utilization;
let shutdown = Arc::clone(&inputs.shutdown);
let consumer = inputs.consumer;
let handle =
thread::spawn(move || run::<Counter>(consumer, control_rx, config, &shutdown));
UringStage {
control,
clients: HashMap::new(),
shutdown: inputs.shutdown,
handle: Some(handle),
}
}
impl StageHarness for UringStage {
fn connect(&mut self, connection_id: u64) {
let (event, client) = connection(connection_id);
self.control.send(event).expect("the stage is running");
self.clients.insert(connection_id, client);
}
fn next_frame(&mut self, connection_id: u64, within: Duration) -> Option<Seen> {
let client = self.clients.get_mut(&connection_id)?;
client
.set_read_timeout(Some(within))
.expect("set read timeout");
read_frame(client).ok()
}
}
impl Drop for UringStage {
fn drop(&mut self) {
self.shutdown.store(true, Ordering::Relaxed);
if let Some(handle) = self.handle.take() {
if handle.join().is_err() && !thread::panicking() {
panic!("the stage panicked");
}
}
}
}
#[test]
fn an_early_reply_waits_out_an_event_split_across_reads_uring() {
an_early_reply_waits_out_an_event_split_across_reads(start_uring);
}
#[test]
fn early_replies_overtake_a_held_write_uring() {
for kind in every_kind() {
early_replies_overtake_a_held_write(start_uring, kind);
}
}
#[test]
fn a_gate_held_inside_an_event_sends_no_early_reply_uring() {
a_gate_held_inside_an_event_sends_no_early_reply(start_uring);
}
#[test]
fn connection_first_seen_inside_a_gate_wait_uring() {
connection_first_seen_inside_a_gate_wait(start_uring);
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(24))]
#[test]
fn every_request_is_answered_once_and_whole_uring(script in script()) {
every_request_is_answered_once_and_whole(start_uring, &script)?;
}
}
}
mod degraded_release {
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, mpsc};
use std::thread;
use std::time::Duration;
use counter_server::{
Counter, CounterQuery, CounterReport, KIND_RESP_ACK, KIND_RESP_REJECTED,
};
use melin_pipeline::ring::DisruptorBuilder;
use melin_pipeline::wait::WaitStrategy;
use melin_transport_core::fence::FenceState;
use melin_transport_core::pipeline::{OutputPayload, OutputSlot, RequestId};
use melin_transport_core::replica_presence::ReplicaPresence;
use melin_transport_core::{DurableWireSeqCursor, WireSeq};
use melin_wire_protocol::control_codec::{TAG_BATCH_END, TAG_BATCH_END_DEGRADED};
use super::refusal_order::{Seen, config, connection, early_id, read_frame, refusal};
use crate::ack_policy::AckPolicy;
use crate::early_reply::early_reply_channel;
use crate::halt::DegradedRelease;
use crate::replication::ReplicationMetrics;
use crate::response::run;
fn run_halted(on: bool, quiet: Duration) -> Vec<Seen> {
let (mut producer, mut consumers) =
DisruptorBuilder::<OutputSlot<CounterReport, CounterQuery>>::new(64)
.add_consumer()
.build(WaitStrategy::SpinThenYield);
let consumer = consumers.pop().expect("one consumer was requested");
let (connected, mut client) = connection(1);
let (mut refusals, refusal_rx) =
early_reply_channel(crate::early_reply::EARLY_REPLY_QUEUE_CAPACITY);
let journal_cursor = DurableWireSeqCursor::detached(WireSeq::new(1));
let mut config = config(
journal_cursor,
Arc::new(FenceState::new(0)),
refusal_rx,
None,
);
config.policy = crate::response::GatePolicy {
start: crate::durability_gate::GateStart::settled(AckPolicy::DiskAndRam),
forced: melin_transport_core::lineage::forced_policy_cell().1,
};
config.replication_metrics = Some(Arc::new(ReplicationMetrics::default()));
config.replica_active = Some([
Arc::new(AtomicBool::new(false)),
Arc::new(AtomicBool::new(false)),
]);
config.degraded_release = on.then(|| DegradedRelease {
presence: Arc::new(ReplicaPresence::new()),
grace: Duration::ZERO,
});
let (control_tx, control_rx) = mpsc::channel();
control_tx.send(connected).expect("channel open");
refusals.try_send(refusal(1, 0)).expect("queue has room");
refusals.try_send(refusal(1, 1)).expect("queue has room");
refusals.flush();
producer.publish(OutputSlot {
connection_id: 1,
request_id: HELD,
input_seq: 0,
wire_seq: 1,
payload: OutputPayload::Report(CounterReport::Ack { new_value: 7 }),
is_last_in_request: true,
..Default::default()
});
let shutdown = AtomicBool::new(false);
let mut seen = Vec::new();
thread::scope(|scope| {
let stage = scope.spawn(|| run::<Counter>(consumer, control_rx, config, &shutdown));
client
.set_read_timeout(Some(quiet))
.expect("set read timeout");
while let Ok(frame) = read_frame(&mut client) {
seen.push(frame);
}
shutdown.store(true, Ordering::Relaxed);
stage.join().expect("stage panicked");
});
seen
}
const HELD: RequestId = RequestId(7);
#[test]
fn a_held_write_is_answered_degraded_and_refusals_end_in_a_plain_batch_end() {
let seen = run_halted(true, Duration::from_millis(500));
assert_eq!(
seen,
[
Seen::App(KIND_RESP_REJECTED),
Seen::Final(TAG_BATCH_END, early_id(0)),
Seen::App(KIND_RESP_REJECTED),
Seen::Final(TAG_BATCH_END, early_id(1)),
Seen::App(KIND_RESP_ACK),
Seen::Final(TAG_BATCH_END_DEGRADED, HELD),
],
"the refusals first, waiting for nothing; only the held write marked"
);
}
#[test]
fn with_the_switch_off_the_held_write_waits_and_the_refusals_do_not() {
let seen = run_halted(false, Duration::from_millis(300));
assert_eq!(
seen,
[
Seen::App(KIND_RESP_REJECTED),
Seen::Final(TAG_BATCH_END, early_id(0)),
Seen::App(KIND_RESP_REJECTED),
Seen::Final(TAG_BATCH_END, early_id(1)),
],
"both refusals go out; the held write does not"
);
}
}
mod stop_while_gated {
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, mpsc};
use std::thread;
use std::time::{Duration, Instant};
use counter_server::{Counter, CounterQuery, CounterReport, KIND_RESP_ACK};
use melin_pipeline::ring::DisruptorBuilder;
use melin_pipeline::wait::WaitStrategy;
use melin_transport_core::fence::FenceState;
use melin_transport_core::pipeline::{OutputPayload, OutputSlot, RequestId};
use melin_transport_core::{DurableWireSeqCursor, WireSeq};
use melin_wire_protocol::control_codec::TAG_BATCH_END;
use super::refusal_order::{Seen, config, connection, read_frame};
use crate::early_reply::early_reply_channel;
use crate::response::run;
const EXIT_TIMEOUT: Duration = Duration::from_secs(10);
fn stop_while_gated(fenced: bool) {
let (mut producer, mut consumers) =
DisruptorBuilder::<OutputSlot<CounterReport, CounterQuery>>::new(64)
.add_consumer()
.build(WaitStrategy::SpinThenYield);
let consumer = consumers.pop().expect("one consumer was requested");
let (connected, mut client) = connection(1);
let (control_tx, control_rx) = mpsc::channel();
control_tx.send(connected).expect("channel open");
let journal_cursor = DurableWireSeqCursor::detached(WireSeq::new(1));
let fence = Arc::new(FenceState::new(0));
let early_replies =
early_reply_channel(crate::early_reply::EARLY_REPLY_QUEUE_CAPACITY).1;
let config = config(journal_cursor, Arc::clone(&fence), early_replies, None);
for wire_seq in [1, 2] {
producer.publish(OutputSlot {
connection_id: 1,
request_id: RequestId(wire_seq),
input_seq: wire_seq - 1,
wire_seq,
payload: OutputPayload::Report(CounterReport::Ack {
new_value: wire_seq,
}),
is_last_in_request: true,
..Default::default()
});
}
let shutdown: &'static AtomicBool = Box::leak(Box::new(AtomicBool::new(false)));
let stage =
thread::spawn(move || run::<Counter>(consumer, control_rx, config, shutdown));
let mut seen = Vec::new();
for _ in 0..2 {
seen.push(read_frame(&mut client).expect("the confirmed reply arrives"));
}
assert_eq!(
seen,
[
Seen::App(KIND_RESP_ACK),
Seen::Final(TAG_BATCH_END, RequestId(1))
]
);
if fenced {
assert!(fence.fence(), "first latch");
}
shutdown.store(true, Ordering::Relaxed);
let deadline = Instant::now() + EXIT_TIMEOUT;
while !stage.is_finished() {
assert!(
Instant::now() < deadline,
"the stage did not leave the gate on shutdown"
);
thread::sleep(Duration::from_millis(10));
}
stage.join().expect("stage panicked");
assert!(
read_frame(&mut client).is_err(),
"a reply the policy never confirmed was sent"
);
}
#[test]
fn a_stopped_stage_leaves_the_gate_and_drops_the_held_reply() {
stop_while_gated(false);
}
#[test]
fn a_fenced_stage_leaves_the_gate_and_drops_the_held_reply() {
stop_while_gated(true);
}
}
mod policy_change {
use std::io::ErrorKind;
use std::os::unix::io::AsRawFd;
use std::os::unix::net::UnixStream;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, mpsc};
use std::thread;
use std::time::{Duration, Instant};
use counter_server::{Counter, CounterQuery, CounterReport, KIND_RESP_ACK};
use melin_journal::AckPolicyChange;
use melin_pipeline::ring::DisruptorBuilder;
use melin_pipeline::wait::WaitStrategy;
use melin_transport_core::fence::FenceState;
use melin_transport_core::lineage::{ForcedPolicy, forced_policy_cell};
use melin_transport_core::pipeline::{OutputPayload, OutputSlot, RequestId};
use melin_transport_core::{DurableWireSeqCursor, WireSeq};
use melin_wire_protocol::control_codec::TAG_BATCH_END;
use super::refusal_order::{Seen, config, connection, read_frame};
use crate::ack_policy::AckPolicy;
use crate::early_reply::early_reply_channel;
use crate::response::run;
const SETTLE_TIMEOUT: Duration = Duration::from_secs(10);
const HOLD: Duration = Duration::from_millis(50);
fn reply(wire_seq: u64) -> OutputSlot<CounterReport, CounterQuery> {
OutputSlot {
connection_id: 1,
request_id: RequestId(wire_seq),
input_seq: wire_seq - 1,
wire_seq,
payload: OutputPayload::Report(CounterReport::Ack {
new_value: wire_seq,
}),
is_last_in_request: true,
..Default::default()
}
}
fn nothing_sent(client: &UnixStream) -> bool {
let mut byte = 0u8;
let n = unsafe {
libc::recv(
client.as_raw_fd(),
(&raw mut byte).cast(),
1,
libc::MSG_PEEK | libc::MSG_DONTWAIT,
)
};
n < 0 && std::io::Error::last_os_error().kind() == ErrorKind::WouldBlock
}
#[test]
fn a_change_slot_sends_nothing_and_a_forced_one_waits_for_its_entry() {
let (mut producer, mut consumers) =
DisruptorBuilder::<OutputSlot<CounterReport, CounterQuery>>::new(64)
.add_consumer()
.build(WaitStrategy::SpinThenYield);
let consumer = consumers.pop().expect("one consumer was requested");
let (connected, mut client) = connection(1);
let (control_tx, control_rx) = mpsc::channel();
control_tx.send(connected).expect("channel open");
let journal_cursor = DurableWireSeqCursor::detached(WireSeq::new(3));
let early_replies =
early_reply_channel(crate::early_reply::EARLY_REPLY_QUEUE_CAPACITY).1;
let mut config = config(
journal_cursor.clone(),
Arc::new(FenceState::new(0)),
early_replies,
None,
);
let (mut forced_writer, forced) = forced_policy_cell();
config.policy.forced = forced;
let utilization = Arc::clone(&config.utilization);
producer.publish(reply(1));
producer.publish(OutputSlot {
connection_id: 1,
input_seq: 1,
wire_seq: 2,
payload: OutputPayload::AckPolicyChange(AckPolicyChange {
policy: AckPolicy::TwoDisks.to_byte(),
unwitnessed: false,
}),
is_last_in_request: true,
..Default::default()
});
producer.publish(reply(3));
let shutdown = AtomicBool::new(false);
thread::scope(|scope| {
let stage = scope.spawn(|| run::<Counter>(consumer, control_rx, config, &shutdown));
let before = [read_frame(&mut client), read_frame(&mut client)];
let deadline = Instant::now() + SETTLE_TIMEOUT;
while utilization.ack_policy_in_force.load() != Some(AckPolicy::TwoDisks.to_byte())
&& Instant::now() < deadline
{
thread::sleep(Duration::from_millis(1));
}
thread::sleep(HOLD);
let the_change_sent_nothing = nothing_sent(&client);
forced_writer.store(ForcedPolicy {
wire_seq: WireSeq::new(4),
policy: AckPolicy::Disk.to_byte(),
});
thread::sleep(HOLD);
let held_until_the_entry_is_on_disk = nothing_sent(&client);
journal_cursor.store(WireSeq::new(4));
let after = [read_frame(&mut client), read_frame(&mut client)];
shutdown.store(true, Ordering::Relaxed);
stage.join().expect("stage panicked");
for (frames, id) in [(before, 1), (after, 3)] {
let frames = frames.map(|frame| frame.expect("a reply arrived"));
assert_eq!(
frames,
[
Seen::App(KIND_RESP_ACK),
Seen::Final(TAG_BATCH_END, RequestId(id))
]
);
}
assert!(
the_change_sent_nothing,
"a frame went out for the change slot, or the reply it holds"
);
assert!(
held_until_the_entry_is_on_disk,
"the forced swap applied before its entry was on disk"
);
});
}
}
mod startup_report {
use std::sync::atomic::AtomicBool;
use std::sync::{Arc, mpsc};
use counter_server::{Counter, CounterQuery, CounterReport};
use melin_pipeline::ring::DisruptorBuilder;
use melin_pipeline::wait::WaitStrategy;
use melin_transport_core::fence::FenceState;
use melin_transport_core::pipeline::OutputSlot;
use melin_transport_core::{DurableWireSeqCursor, WireSeq};
use super::refusal_order::config;
use crate::connection_limit::RingSizing;
use crate::early_reply::early_reply_channel;
use crate::response::run;
#[test]
fn a_ring_that_cannot_be_created_is_reported_and_the_stage_returns() {
let (_producer, mut consumers) =
DisruptorBuilder::<OutputSlot<CounterReport, CounterQuery>>::new(64)
.add_consumer()
.build(WaitStrategy::SpinThenYield);
let consumer = consumers.pop().expect("one consumer was requested");
let (_control_tx, control_rx) = mpsc::channel();
let early_replies =
early_reply_channel(crate::early_reply::EARLY_REPLY_QUEUE_CAPACITY).1;
let (ready_tx, ready_rx) = mpsc::sync_channel(1);
let mut config = config(
DurableWireSeqCursor::detached(WireSeq::new(0)),
Arc::new(FenceState::new(0)),
early_replies,
None,
);
config.ring_sizing = RingSizing::unchecked(0, 32);
config.ready = Some(ready_tx);
let started = run::<Counter>(consumer, control_rx, config, &AtomicBool::new(false));
assert!(!started, "the stage reports that it never ran");
let err = ready_rx
.recv()
.expect("the stage reports")
.expect_err("a zero-entry ring cannot be created");
let msg = err.to_string();
assert!(msg.contains("response"), "{msg}");
assert!(msg.contains("io_uring"), "{msg}");
}
}
}