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::durability_gate::{Admission, Backing, BatchEnds, DurabilityGate, GateInputs};
pub use crate::durability_gate::{GatePolicy, GateStart};
use crate::halt::{Refusal, RefusalQueue};
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::{EncodeBuf, MAX_APP_FRAME, encode_slot_payload, frame_app_response};
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 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 refusals: RefusalQueue<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,
}
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 refusals,
ring_sizing,
ready,
degraded_release,
#[cfg(test)]
mut pause_after_control_drain,
} = config;
let ring_entries = ring_sizing.ring_entries();
let mut 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 connections: FxHashMap<u64, ConnectionEntry> =
FxHashMap::with_capacity_and_hasher(256, Default::default());
let mut encode_buf: EncodeBuf = [0u8; MAX_APP_FRAME];
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 mut 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 mut egress_rec = trace::register_stage("response: egress (flush_sends elapsed)");
#[cfg(feature = "latency-trace")]
let mut last_stats_flush = Instant::now();
#[cfg(feature = "latency-trace")]
let mut pending_e2e: Vec<(u64, trace::MonoTraceInstant)> = Vec::with_capacity(MAX_BATCH);
let mut dirty_connections: Vec<u64> = Vec::with_capacity(256);
let mut flush = FlushCadence::default();
let mut to_remove: Vec<u64> = Vec::new();
let mut cqes: Vec<(u64, i32)> = Vec::with_capacity(ring_entries as usize);
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 server_busy_wire_frame = {
let mut buf = [0u8; 8];
let written =
control_codec::encode_transport_response(&TransportResponse::ServerBusy, &mut buf)
.expect("ServerBusy encodes");
buf[..written].to_vec()
};
let 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 last_input_seq = u64::MAX;
'run: loop {
if shutdown.load(Ordering::Relaxed) {
let flush = !fence_state.is_fenced();
if flush && !dirty_connections.is_empty() {
flush_sends(
&mut ring,
&mut connections,
&mut dirty_connections,
&mut to_remove,
&mut cqes,
);
#[cfg(feature = "latency-trace")]
close_e2e_samples(&mut pending_e2e, &mut 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 idle_refusal_bound = refusals.idle_bound();
let slots = consumer.read_contiguous(MAX_BATCH);
let refusals_waiting = refusals.sync();
while let Ok(event) = control_rx.try_recv() {
match event {
ControlEvent::Connected {
connection_id,
fd,
writer,
} => {
let owner: Box<dyn Send> = Box::new(writer);
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 connections.remove(&connection_id).is_some() {
active_connections.fetch_sub(1, Ordering::Relaxed);
}
unmark_dirty(connection_id, &mut dirty_connections);
#[cfg(feature = "latency-trace")]
discard_e2e_samples(&mut pending_e2e, &[connection_id]);
}
ControlEvent::PipelineBusy { connection_id } => {
if let Some(entry) = connections.get_mut(&connection_id) {
if entry.send_buf.len() + server_busy_wire_frame.len() <= MAX_SEND_BUF {
entry.send_buf.extend_from_slice(&server_busy_wire_frame);
entry.last_send = Instant::now();
mark_dirty(entry, connection_id, &mut dirty_connections);
}
}
}
}
}
#[cfg(test)]
if let Some(pause) = pause_after_control_drain.as_mut() {
pause();
}
if slots.is_empty() {
if let Some(bound) = idle_refusal_bound {
let now = Instant::now();
refusals.release(bound, |refusal| {
append_refusal(
refusal,
&*encoder,
&mut connections,
&mut encode_buf,
&batch_ends.policy,
now,
&mut dirty_connections,
&mut to_remove,
&mut flush,
);
});
#[cfg(feature = "latency-trace")]
discard_e2e_samples(&mut pending_e2e, &to_remove);
for conn_id in to_remove.drain(..) {
if let Some(entry) = connections.remove(&conn_id) {
teardown_dropped(entry, &active_connections);
}
unmark_dirty(conn_id, &mut dirty_connections);
}
}
if !dirty_connections.is_empty() {
#[cfg(feature = "tick-to-trade")]
let egress_start = trace::mono_trace_ns();
flush_sends(
&mut ring,
&mut connections,
&mut dirty_connections,
&mut to_remove,
&mut cqes,
);
flush.on_flush();
#[cfg(feature = "tick-to-trade")]
egress_rec.record_elapsed(egress_start, trace::mono_trace_ns());
#[cfg(feature = "latency-trace")]
close_e2e_samples(&mut pending_e2e, &mut server_e2e_rec);
for conn_id in to_remove.drain(..) {
if let Some(entry) = connections.remove(&conn_id) {
teardown_dropped(entry, &active_connections);
}
}
}
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 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 dirty_connections);
}
}
if !dirty_connections.is_empty() {
flush_sends(
&mut ring,
&mut connections,
&mut dirty_connections,
&mut to_remove,
&mut cqes,
);
flush.on_flush();
for conn_id in to_remove.drain(..) {
if let Some(entry) = connections.remove(&conn_id) {
teardown_dropped(entry, &active_connections);
}
}
}
}
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();
server_e2e_rec.flush();
#[cfg(feature = "tick-to-trade")]
{
journal_wait_rec.flush();
replica_wait_rec.flush();
encode_rec.flush();
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 first_of_event = slot.input_seq != last_input_seq;
last_input_seq = slot.input_seq;
if refusals_waiting && first_of_event {
refusals.release(slot.input_seq, |refusal| {
append_refusal(
refusal,
&*encoder,
&mut connections,
&mut encode_buf,
&batch_ends.policy,
batch_now,
&mut dirty_connections,
&mut to_remove,
&mut flush,
);
});
}
#[cfg(feature = "tick-to-trade")]
let mut gate_tracker = GateCrossTracker::new(slot.wire_seq);
let (destination, backing) = match gate.admit(
slot,
shutdown,
#[cfg(feature = "tick-to-trade")]
&mut gate_tracker,
|| {
if !dirty_connections.is_empty() {
#[cfg(feature = "tick-to-trade")]
let egress_start = trace::mono_trace_ns();
flush_sends(
&mut ring,
&mut connections,
&mut dirty_connections,
&mut to_remove,
&mut cqes,
);
flush.on_flush();
#[cfg(feature = "tick-to-trade")]
egress_rec.record_elapsed(egress_start, trace::mono_trace_ns());
#[cfg(feature = "latency-trace")]
close_e2e_samples(&mut pending_e2e, &mut server_e2e_rec);
for conn_id in to_remove.drain(..) {
if let Some(entry) = connections.remove(&conn_id) {
teardown_dropped(entry, &active_connections);
}
}
}
},
) {
Admission::Reply {
connection_id,
backing,
} => (Some(connection_id), backing),
Admission::GateOnly => (None, Backing::Policy),
Admission::Shutdown => continue 'run,
};
#[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| 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, &batch_ends)
} 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 dirty_connections,
&mut 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) {
pending_e2e.push((connection_id, slot.recv_ts));
}
flush.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"
);
}
if flush.is_due() {
flush.on_flush();
#[cfg(feature = "latency-trace")]
discard_e2e_samples(&mut pending_e2e, &to_remove);
for conn_id in to_remove.drain(..) {
if let Some(entry) = connections.remove(&conn_id) {
teardown_dropped(entry, &active_connections);
}
unmark_dirty(conn_id, &mut dirty_connections);
}
#[cfg(feature = "tick-to-trade")]
let egress_start = trace::mono_trace_ns();
flush_sends(
&mut ring,
&mut connections,
&mut dirty_connections,
&mut to_remove,
&mut cqes,
);
#[cfg(feature = "tick-to-trade")]
egress_rec.record_elapsed(egress_start, trace::mono_trace_ns());
#[cfg(feature = "latency-trace")]
close_e2e_samples(&mut pending_e2e, &mut server_e2e_rec);
for conn_id in to_remove.drain(..) {
if let Some(entry) = connections.remove(&conn_id) {
teardown_dropped(entry, &active_connections);
}
}
}
}
consumer.commit();
#[cfg(feature = "latency-trace")]
discard_e2e_samples(&mut pending_e2e, &to_remove);
for conn_id in to_remove.drain(..) {
if let Some(entry) = connections.remove(&conn_id) {
teardown_dropped(entry, &active_connections);
}
unmark_dirty(conn_id, &mut dirty_connections);
}
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_refusal<R: Copy, Q: Copy>(
refusal: Refusal<R>,
encoder: &dyn melin_app::encoder::ResponseEncoder<Report = R, Query = Q>,
connections: &mut FxHashMap<u64, ConnectionEntry>,
encode_buf: &mut EncodeBuf,
batch_end_wire_frame: &[u8],
now: Instant,
dirty_connections: &mut Vec<u64>,
to_remove: &mut Vec<u64>,
flush: &mut FlushCadence,
) {
let Some(entry) = connections.get_mut(&refusal.connection_id) else {
debug!(
connection_id = refusal.connection_id,
"refusal dropped: connection not registered"
);
return;
};
let payload = frame_app_response(encode_buf, |body| {
encoder.encode_report(&refusal.report, body)
});
append_frames(
Some(payload),
batch_end_wire_frame,
refusal.connection_id,
entry,
encode_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,
}
}
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 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::response::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)),
refusals: crate::halt::refusal_channel(Arc::new(
melin_pipeline::padding::CachePadded::new(AtomicU64::new(0)),
))
.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::{ErrorKind, 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, KIND_RESP_REJECTED,
ResponseEncoder,
};
use melin_pipeline::padding::CachePadded;
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, TAG_BATCH_END};
use crate::ControlEvent;
use crate::ack_policy::AckPolicy;
use crate::halt::{Refusal, RefusalQueue, refusal_channel};
use crate::response::{Response, run};
const READ_TIMEOUT: Duration = Duration::from_secs(5);
const QUIET: Duration = Duration::from_millis(100);
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum Seen {
App(u8),
Protocol(u8),
}
impl Seen {
pub(crate) fn of(payload: &[u8]) -> Self {
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(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_waits_for_earlier_replies_and_precedes_later_ones() {
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_sock) = connection(1);
let matching_progress = Arc::new(CachePadded::new(AtomicU64::new(0)));
let (mut refusals, refusal_rx) = refusal_channel(Arc::clone(&matching_progress));
let journal_cursor = DurableWireSeqCursor::detached(WireSeq::new(0));
let (control_tx, control_rx) = mpsc::channel();
control_tx.send(connected).expect("channel open");
let shutdown = AtomicBool::new(false);
let config = config(
journal_cursor.clone(),
Arc::new(FenceState::new(0)),
refusal_rx,
None,
);
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,
input_seq: 0,
wire_seq: 1,
payload: OutputPayload::Report(CounterReport::Ack { new_value: 7 }),
is_last_in_request: true,
..Default::default()
});
matching_progress.get().store(1, Ordering::Release);
thread::scope(|scope| {
let stage = scope.spawn(|| run::<Counter>(consumer, control_rx, config, &shutdown));
let mut seen = Vec::new();
let first = read_into(&mut client_sock, &mut seen)
&& read_into(&mut client_sock, &mut seen);
client_sock
.set_read_timeout(Some(QUIET))
.expect("set read timeout");
let mut probe = [0u8; 1];
let early = !matches!(
client_sock.read(&mut probe),
Err(e) if matches!(e.kind(), ErrorKind::WouldBlock | ErrorKind::TimedOut)
);
client_sock
.set_read_timeout(Some(READ_TIMEOUT))
.expect("set read timeout");
journal_cursor.store(WireSeq::new(1));
let rest = !early && (0..4).all(|_| read_into(&mut client_sock, &mut seen));
shutdown.store(true, Ordering::Relaxed);
stage.join().expect("stage panicked");
assert!(first, "the refusal ahead of the held event never arrived");
assert!(
!early,
"the later refusal overtook the reply the gate was holding"
);
assert!(rest, "replies missing after the gate opened: {seen:x?}");
assert_eq!(
seen,
[
Seen::App(KIND_RESP_REJECTED),
Seen::Protocol(TAG_BATCH_END),
Seen::App(KIND_RESP_ACK),
Seen::Protocol(TAG_BATCH_END),
Seen::App(KIND_RESP_REJECTED),
Seen::Protocol(TAG_BATCH_END),
],
"replies out of request order"
);
});
}
#[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 progress = Arc::new(CachePadded::new(AtomicU64::new(0)));
let (mut refusals, refusal_rx) = refusal_channel(progress);
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::Protocol(TAG_BATCH_END)]
);
}
});
}
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, input_seq: u64) -> Refusal<CounterReport> {
Refusal {
connection_id,
input_seq,
report: CounterReport::Rejected,
}
}
pub(super) fn config(
journal_cursor: DurableWireSeqCursor,
fence_state: Arc<FenceState>,
refusals: RefusalQueue<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::response::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)),
refusals,
ring_sizing: crate::connection_limit::RingSizing::for_max_connections(4)
.expect("a supported cap"),
ready: None,
degraded_release: None,
pause_after_control_drain,
}
}
}
mod degraded_release {
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, KIND_RESP_REJECTED,
};
use melin_pipeline::padding::CachePadded;
use melin_pipeline::ring::DisruptorBuilder;
use melin_pipeline::wait::WaitStrategy;
use melin_transport_core::fence::FenceState;
use melin_transport_core::pipeline::{OutputPayload, OutputSlot};
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, read_frame, refusal};
use crate::ack_policy::AckPolicy;
use crate::halt::{DegradedRelease, refusal_channel};
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 matching_progress = Arc::new(CachePadded::new(AtomicU64::new(0)));
let (mut refusals, refusal_rx) = refusal_channel(Arc::clone(&matching_progress));
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::response::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,
input_seq: 0,
wire_seq: 1,
payload: OutputPayload::Report(CounterReport::Ack { new_value: 7 }),
is_last_in_request: true,
..Default::default()
});
matching_progress.get().store(1, Ordering::Release);
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
}
#[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::Protocol(TAG_BATCH_END),
Seen::App(KIND_RESP_ACK),
Seen::Protocol(TAG_BATCH_END_DEGRADED),
Seen::App(KIND_RESP_REJECTED),
Seen::Protocol(TAG_BATCH_END),
],
"in request order, only the held write marked"
);
}
#[test]
fn with_the_switch_off_the_held_write_and_the_refusal_behind_it_wait() {
let seen = run_halted(false, Duration::from_millis(300));
assert_eq!(
seen,
[Seen::App(KIND_RESP_REJECTED), Seen::Protocol(TAG_BATCH_END)],
"only the refusal ahead of the held write goes out"
);
}
}
mod stop_while_gated {
use std::sync::atomic::{AtomicBool, AtomicU64, 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::padding::CachePadded;
use melin_pipeline::ring::DisruptorBuilder;
use melin_pipeline::wait::WaitStrategy;
use melin_transport_core::fence::FenceState;
use melin_transport_core::pipeline::{OutputPayload, OutputSlot};
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::halt::refusal_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 refusals = refusal_channel(Arc::new(CachePadded::new(AtomicU64::new(0)))).1;
let config = config(journal_cursor, Arc::clone(&fence), refusals, None);
for wire_seq in [1, 2] {
producer.publish(OutputSlot {
connection_id: 1,
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::Protocol(TAG_BATCH_END)]
);
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, AtomicU64, 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::padding::CachePadded;
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};
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::halt::refusal_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,
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 refusals = refusal_channel(Arc::new(CachePadded::new(AtomicU64::new(0)))).1;
let mut config = config(
journal_cursor.clone(),
Arc::new(FenceState::new(0)),
refusals,
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 in [before, after] {
let frames = frames.map(|frame| frame.expect("a reply arrived"));
assert_eq!(
frames,
[Seen::App(KIND_RESP_ACK), Seen::Protocol(TAG_BATCH_END)]
);
}
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, AtomicU64};
use std::sync::{Arc, mpsc};
use counter_server::{Counter, CounterQuery, CounterReport};
use melin_pipeline::padding::CachePadded;
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::halt::refusal_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 refusals = refusal_channel(Arc::new(CachePadded::new(AtomicU64::new(0)))).1;
let (ready_tx, ready_rx) = mpsc::sync_channel(1);
let mut config = config(
DurableWireSeqCursor::detached(WireSeq::new(0)),
Arc::new(FenceState::new(0)),
refusals,
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}");
}
}
}