use rustc_hash::FxHashMap;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::mpsc;
use std::time::{Duration, Instant};
use melin_pipeline::ring;
use melin_pipeline::spsc;
use melin_pipeline::wait::WaitStrategy;
use crate::batch_boundary::{AtBoundary, BatchBoundary};
use crate::durability_gate::{
Admission, Backing, BatchEnds, DurabilityGate, GateInputs, GatePolicy, GateWait,
};
use crate::early_reply::{EarlyReplyQueue, Registered};
use melin_app::Application;
use melin_app::amortized_timer::AmortizedTimer;
use melin_transport_core::DurableWireSeqCursor;
use melin_transport_core::pipeline::{OutputSlot, StageUtilization};
use melin_wire_protocol::control::TransportResponse;
use melin_wire_protocol::control_codec::{self, BATCH_END_FRAME_LEN};
use crate::response_frame::{
EarlyReplyBuf, EncodeBuf, MAX_APP_FRAME, MAX_EARLY_REPLY, encode_early_reply,
encode_slot_payload,
};
#[cfg(feature = "latency-trace")]
use melin_transport_core::trace;
const MAX_BATCH: usize = 1024;
const MAX_TX_FRAME: usize = MAX_APP_FRAME + BATCH_END_FRAME_LEN;
const _: () = assert!(MAX_EARLY_REPLY <= MAX_TX_FRAME);
#[derive(Clone, Copy)]
pub struct TxFrame {
pub connection_id: u64,
pub len: u16,
pub data: [u8; MAX_TX_FRAME],
}
impl Default for TxFrame {
fn default() -> Self {
TxFrame {
connection_id: 0,
len: 0,
data: [0u8; MAX_TX_FRAME],
}
}
}
impl TxFrame {
pub fn as_bytes(&self) -> &[u8] {
&self.data[..self.len as usize]
}
}
pub enum ControlEvent {
Connected { connection_id: u64 },
Disconnected { connection_id: u64 },
}
#[allow(clippy::too_many_arguments)]
pub fn run<A: Application>(
mut consumer: ring::Consumer<OutputSlot<A::Report, A::QueryResponse>>,
control_rx: mpsc::Receiver<ControlEvent>,
journal_persisted_wire_seq: DurableWireSeqCursor,
policy: GatePolicy,
replication_metrics: Option<Arc<crate::replication::ReplicationMetrics>>,
replica_active: Option<[Arc<AtomicBool>; 2]>,
shutdown: &AtomicBool,
heartbeat_interval: Option<Duration>,
active_connections: Arc<AtomicU64>,
mut tx_producers: Vec<spsc::Producer<TxFrame>>,
utilization: Arc<StageUtilization>,
wait: WaitStrategy,
encoder: crate::response::ResponseEncoderArc<A>,
mut early_replies: EarlyReplyQueue<A::Report>,
degraded_release: Option<crate::halt::DegradedRelease>,
#[cfg(test)] mut pause_after_control_drain: Option<Box<dyn FnMut() + Send>>,
) {
let mut gate = DurabilityGate::new(
GateInputs {
journal_persisted_wire_seq,
replication_metrics,
replica_active,
utilization: Arc::clone(&utilization),
wait,
degraded_release,
},
policy,
"dpdk response",
);
let mut connections: FxHashMap<u64, ConnectionHeartbeat> =
FxHashMap::with_capacity_and_hasher(256, Default::default());
let mut encode_buf: EncodeBuf = [0u8; MAX_APP_FRAME];
let mut early_buf: EarlyReplyBuf = [0u8; MAX_EARLY_REPLY];
let mut batch_ends = BatchEnds::new();
let mut heartbeat_frame = [0u8; 8];
let heartbeat_len = control_codec::encode_transport_response(
&TransportResponse::Heartbeat,
&mut heartbeat_frame,
)
.expect("heartbeat encodes");
let mut heartbeat_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();
#[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 → SPSC publish)");
#[cfg(feature = "latency-trace")]
let mut server_e2e_rec =
trace::register_stage("server e2e (reader recv → response SPSC publish)");
#[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 = "latency-trace")]
let mut last_stats_flush = Instant::now();
'run: loop {
if shutdown.load(Ordering::Relaxed) {
utilization.busy.store(busy_count, Ordering::Relaxed);
utilization.idle.store(idle_count, Ordering::Relaxed);
return;
}
let slots = consumer.read_contiguous(MAX_BATCH);
let mut registered = early_replies.register(|| {
process_control_events(
&control_rx,
&mut connections,
&active_connections,
Instant::now,
)
});
#[cfg(test)]
if let Some(pause) = pause_after_control_drain.as_mut() {
pause();
}
if slots.is_empty() {
if let Some(at) = boundary.at_boundary() {
release_early_replies(
&mut registered,
&at,
&*encoder,
&mut connections,
&mut tx_producers,
&mut early_buf,
Instant::now,
);
}
idle_count += 1;
if idle_count.is_multiple_of(1024) {
utilization.busy.store(busy_count, Ordering::Relaxed);
utilization.idle.store(idle_count, Ordering::Relaxed);
}
if let Some(interval) = heartbeat_interval
&& heartbeat_timer
.tick(Duration::from_secs(1), waiter.spinning())
.is_some()
{
let now = Instant::now();
let mut failed: Vec<u64> = Vec::new();
for (&conn_id, state) in connections.iter_mut() {
if now.duration_since(state.last_send) >= interval {
let mut frame = TxFrame {
connection_id: conn_id,
len: heartbeat_len as u16,
..Default::default()
};
frame.data[..heartbeat_len]
.copy_from_slice(&heartbeat_frame[..heartbeat_len]);
let tid = (conn_id >> 56) as usize % tx_producers.len();
if tx_producers[tid].try_publish(frame).is_err() {
failed.push(conn_id);
continue;
}
state.last_send = now;
}
}
for conn_id in failed {
connections.remove(&conn_id);
active_connections.fetch_sub(1, Ordering::Relaxed);
}
}
{
let now_ts = Instant::now();
gate.idle_recheck(now_ts);
#[cfg(feature = "latency-trace")]
if now_ts.duration_since(last_stats_flush) >= trace::IDLE_FLUSH_INTERVAL {
last_stats_flush = now_ts;
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();
}
}
}
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 {
release_early_replies(
&mut registered,
at,
&*encoder,
&mut connections,
&mut tx_producers,
&mut early_buf,
|| batch_now,
);
}
#[cfg(feature = "tick-to-trade")]
let mut gate_tracker = crate::response::GateCrossTracker::new(slot.wire_seq);
let admission = gate.admit(
slot,
shutdown,
#[cfg(feature = "tick-to-trade")]
&mut gate_tracker,
&mut GateWaitHook {
at_boundary,
registered: &mut registered,
control_rx: &control_rx,
connections: &mut connections,
active_connections: &active_connections,
tx_producers: &mut tx_producers,
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);
}
let Some((conn_id, conn_state)) =
destination.and_then(|id| connections.get_mut(&id).map(|state| (id, state)))
else {
if let Some(connection_id) = destination.filter(|&id| id != 0) {
tracing::debug!(
connection_id,
wire_seq = slot.wire_seq,
"reply dropped: connection not registered"
);
}
continue;
};
let tid = (conn_id >> 56) as usize % tx_producers.len();
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();
push_frame(
payload_result,
conn_id,
&mut tx_producers[tid],
&encode_buf,
trailer,
);
#[cfg(feature = "tick-to-trade")]
encode_rec.record_elapsed(encode_start, trace::mono_trace_ns());
tx_producers[tid].flush();
#[cfg(feature = "latency-trace")]
if slot.is_last_in_request {
server_e2e_rec.record_elapsed(slot.recv_ts, trace::mono_trace_ns());
}
conn_state.last_send = batch_now;
}
consumer.commit();
gate.after_batch();
#[cfg(feature = "latency-trace")]
dispatch_rec.record_elapsed(consume_ts, trace::mono_trace_ns());
}
}
#[inline]
fn push_frame(
payload: Option<Result<usize, &'static str>>,
conn_id: u64,
tx: &mut spsc::Producer<TxFrame>,
encode_buf: &[u8],
trailer: &[u8],
) {
let written = match payload {
Some(Ok(n)) => n,
Some(Err(reason)) => {
tracing::error!(connection_id = conn_id, reason, "encode error");
0
}
None => 0,
};
if written == 0 && trailer.is_empty() {
return;
}
let total = written + trailer.len();
assert!(
total <= MAX_TX_FRAME,
"a {written}-byte frame and a {}-byte trailer overrun a {MAX_TX_FRAME}-byte TX slot",
trailer.len()
);
let len = total as u16;
tx.push_with(|frame| {
frame.connection_id = conn_id;
frame.len = len;
frame.data[..written].copy_from_slice(&encode_buf[..written]);
frame.data[written..total].copy_from_slice(trailer);
});
}
fn push_early_reply<R: Copy, Q: Copy>(
reply: crate::early_reply::EarlyReply<R>,
encoder: &dyn melin_app::encoder::ResponseEncoder<Report = R, Query = Q>,
connections: &mut FxHashMap<u64, ConnectionHeartbeat>,
tx_producers: &mut [spsc::Producer<TxFrame>],
early_buf: &mut EarlyReplyBuf,
now: Instant,
) {
let conn_id = reply.connection_id;
let Some(conn_state) = connections.get_mut(&conn_id) else {
tracing::debug!(
connection_id = conn_id,
"early reply dropped: connection not registered"
);
return;
};
let tid = (conn_id >> 56) as usize % tx_producers.len();
let written = encode_early_reply(&reply, encoder, early_buf);
push_frame(
Some(Ok(written)),
conn_id,
&mut tx_producers[tid],
early_buf,
&[],
);
tx_producers[tid].flush();
conn_state.last_send = now;
}
#[allow(clippy::too_many_arguments)]
fn release_early_replies<R: Copy, Q: Copy>(
registered: &mut Registered<'_, R>,
at: &AtBoundary<'_>,
encoder: &dyn melin_app::encoder::ResponseEncoder<Report = R, Query = Q>,
connections: &mut FxHashMap<u64, ConnectionHeartbeat>,
tx_producers: &mut [spsc::Producer<TxFrame>],
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);
push_early_reply(reply, encoder, connections, tx_producers, early_buf, now);
})
}
struct GateWaitHook<'a, 'q, R: Copy, Q: Copy> {
at_boundary: Option<AtBoundary<'a>>,
registered: &'a mut Registered<'q, R>,
control_rx: &'a mpsc::Receiver<ControlEvent>,
connections: &'a mut FxHashMap<u64, ConnectionHeartbeat>,
active_connections: &'a AtomicU64,
tx_producers: &'a mut [spsc::Producer<TxFrame>],
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) {}
fn while_waiting(&mut self) {
let Some(at) = &self.at_boundary else {
return;
};
let connections = &mut *self.connections;
let (control_rx, active_connections) = (self.control_rx, self.active_connections);
self.registered.reregister(|| {
process_control_events(control_rx, connections, active_connections, Instant::now)
});
release_early_replies(
self.registered,
at,
self.encoder,
self.connections,
self.tx_producers,
self.early_buf,
Instant::now,
);
}
}
struct ConnectionHeartbeat {
last_send: Instant,
}
fn process_control_events(
control_rx: &mpsc::Receiver<ControlEvent>,
connections: &mut FxHashMap<u64, ConnectionHeartbeat>,
active_connections: &AtomicU64,
clock: impl Fn() -> Instant,
) {
let mut now = None;
while let Ok(event) = control_rx.try_recv() {
match event {
ControlEvent::Connected { connection_id } => {
let last_send = *now.get_or_insert_with(&clock);
connections.insert(connection_id, ConnectionHeartbeat { last_send });
}
ControlEvent::Disconnected { connection_id } => {
if connections.remove(&connection_id).is_some() {
active_connections.fetch_sub(1, Ordering::Relaxed);
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::AtomicU64;
use std::sync::mpsc;
use std::time::Instant;
#[test]
fn active_connections_single_lifecycle() {
let counter = AtomicU64::new(0);
let (tx, rx) = mpsc::channel();
let mut connections = FxHashMap::default();
let now = Instant::now();
counter.fetch_add(1, Ordering::Relaxed);
tx.send(ControlEvent::Connected { connection_id: 1 })
.unwrap();
process_control_events(&rx, &mut connections, &counter, || now);
assert_eq!(counter.load(Ordering::Relaxed), 1);
assert_eq!(connections.len(), 1);
tx.send(ControlEvent::Disconnected { connection_id: 1 })
.unwrap();
process_control_events(&rx, &mut connections, &counter, || now);
assert_eq!(counter.load(Ordering::Relaxed), 0);
assert_eq!(connections.len(), 0);
}
#[test]
fn disconnect_unknown_connection_no_decrement() {
let counter = AtomicU64::new(0);
let (tx, rx) = mpsc::channel();
let mut connections = FxHashMap::default();
let now = Instant::now();
tx.send(ControlEvent::Disconnected { connection_id: 999 })
.unwrap();
process_control_events(&rx, &mut connections, &counter, || now);
assert_eq!(counter.load(Ordering::Relaxed), 0);
}
#[test]
fn active_connections_multiple_lifecycle() {
let counter = AtomicU64::new(0);
let (tx, rx) = mpsc::channel();
let mut connections = FxHashMap::default();
let now = Instant::now();
for id in 1..=3 {
counter.fetch_add(1, Ordering::Relaxed);
tx.send(ControlEvent::Connected { connection_id: id })
.unwrap();
}
process_control_events(&rx, &mut connections, &counter, || now);
assert_eq!(counter.load(Ordering::Relaxed), 3);
assert_eq!(connections.len(), 3);
tx.send(ControlEvent::Disconnected { connection_id: 2 })
.unwrap();
process_control_events(&rx, &mut connections, &counter, || now);
assert_eq!(counter.load(Ordering::Relaxed), 2);
assert_eq!(connections.len(), 2);
tx.send(ControlEvent::Disconnected { connection_id: 1 })
.unwrap();
tx.send(ControlEvent::Disconnected { connection_id: 3 })
.unwrap();
process_control_events(&rx, &mut connections, &counter, || now);
assert_eq!(counter.load(Ordering::Relaxed), 0);
assert_eq!(connections.len(), 0);
}
#[test]
fn duplicate_disconnect_single_decrement() {
let counter = AtomicU64::new(0);
let (tx, rx) = mpsc::channel();
let mut connections = FxHashMap::default();
let now = Instant::now();
counter.fetch_add(1, Ordering::Relaxed);
tx.send(ControlEvent::Connected { connection_id: 1 })
.unwrap();
process_control_events(&rx, &mut connections, &counter, || now);
tx.send(ControlEvent::Disconnected { connection_id: 1 })
.unwrap();
tx.send(ControlEvent::Disconnected { connection_id: 1 })
.unwrap();
process_control_events(&rx, &mut connections, &counter, || now);
assert_eq!(counter.load(Ordering::Relaxed), 0);
}
#[test]
fn connected_stamps_the_clock_read_lazily_at_drain() {
let counter = AtomicU64::new(0);
let (tx, rx) = mpsc::channel();
let mut connections = FxHashMap::default();
let reads = std::cell::Cell::new(0u32);
let at_drain = Instant::now() + std::time::Duration::from_secs(60);
let clock = || {
reads.set(reads.get() + 1);
at_drain
};
process_control_events(&rx, &mut connections, &counter, clock);
tx.send(ControlEvent::Disconnected { connection_id: 9 })
.unwrap();
process_control_events(&rx, &mut connections, &counter, clock);
assert_eq!(reads.get(), 0);
tx.send(ControlEvent::Connected { connection_id: 1 })
.unwrap();
tx.send(ControlEvent::Connected { connection_id: 2 })
.unwrap();
process_control_events(&rx, &mut connections, &counter, clock);
assert_eq!(reads.get(), 1);
assert_eq!(connections[&1].last_send, at_drain);
assert_eq!(connections[&2].last_send, at_drain);
}
#[test]
fn a_change_slot_queues_no_frame_for_its_connection() {
use counter_server::{Counter, CounterQuery, CounterReport, ResponseEncoder};
use melin_journal::AckPolicyChange;
use melin_pipeline::ring::DisruptorBuilder;
use melin_transport_core::WireSeq;
use melin_transport_core::lineage::forced_policy_cell;
use melin_transport_core::pipeline::OutputPayload;
use crate::ack_policy::AckPolicy;
use crate::durability_gate::GateStart;
use crate::early_reply::early_reply_channel;
const SETTLE_TIMEOUT: Duration = Duration::from_secs(10);
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()
}
}
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 (control_tx, control_rx) = mpsc::channel();
control_tx
.send(ControlEvent::Connected { connection_id: 1 })
.expect("channel open");
let (tx, mut rx) = spsc::channel::<TxFrame>(8, WaitStrategy::SpinThenYield);
let early_replies = early_reply_channel(crate::early_reply::EARLY_REPLY_QUEUE_CAPACITY).1;
producer.publish(reply(1));
producer.publish(OutputSlot {
connection_id: 1,
input_seq: 1,
wire_seq: 2,
payload: OutputPayload::AckPolicyChange(AckPolicyChange {
policy: AckPolicy::Disk.to_byte(),
unwitnessed: false,
}),
is_last_in_request: true,
..Default::default()
});
producer.publish(reply(3));
let shutdown = AtomicBool::new(false);
std::thread::scope(|scope| {
let stage = scope.spawn(|| {
run::<Counter>(
consumer,
control_rx,
DurableWireSeqCursor::detached(WireSeq::new(3)),
GatePolicy {
start: GateStart::settled(AckPolicy::Disk),
forced: forced_policy_cell().1,
},
None,
None,
&shutdown,
None,
Arc::new(AtomicU64::new(0)),
vec![tx],
Arc::new(StageUtilization::default()),
WaitStrategy::SpinThenYield,
Arc::new(ResponseEncoder),
early_replies,
None,
None,
)
});
let mut frames = Vec::new();
let deadline = Instant::now() + SETTLE_TIMEOUT;
while frames.len() < 2 && Instant::now() < deadline {
match rx.try_consume() {
Some((_, frame)) => frames.push(frame.connection_id),
None => std::thread::yield_now(),
}
}
shutdown.store(true, Ordering::Relaxed);
stage.join().expect("stage panicked");
while let Some((_, frame)) = rx.try_consume() {
frames.push(frame.connection_id);
}
assert_eq!(
frames,
[1, 1],
"one frame per reply, and none for the policy change"
);
});
}
mod degraded_release {
use super::*;
use counter_server::{
Counter, CounterQuery, CounterReport, KIND_RESP_ACK, KIND_RESP_REJECTED,
ResponseEncoder,
};
use melin_pipeline::ring::DisruptorBuilder;
use melin_transport_core::WireSeq;
use melin_transport_core::lineage::forced_policy_cell;
use melin_transport_core::pipeline::{OutputPayload, RequestId};
use melin_transport_core::replica_presence::ReplicaPresence;
use melin_wire_protocol::control::RefusalKind;
use melin_wire_protocol::control_codec::{TAG_BATCH_END, TAG_BATCH_END_DEGRADED};
use crate::ack_policy::AckPolicy;
use crate::durability_gate::GateStart;
use crate::early_reply::{EarlyKind, early_reply_channel};
use crate::halt::DegradedRelease;
use crate::replication::ReplicationMetrics;
use crate::response::tests::refusal_order::{Seen, early, early_id, frames_of};
const HELD: RequestId = RequestId(7);
const REJECTED: EarlyKind<CounterReport> = EarlyKind::Rejected(CounterReport::Rejected);
pub(super) fn wire_frames(bytes: &[u8], seen: &mut Vec<Seen>) {
let mut rest = bytes;
while !rest.is_empty() {
let (len, tail) = rest.split_first_chunk::<4>().expect("a length prefix");
let (payload, tail) = tail.split_at(u32::from_le_bytes(*len) as usize);
seen.push(Seen::of(payload));
rest = tail;
}
}
fn run_halted(on: bool, quiet: Duration, kind: EarlyKind<CounterReport>) -> 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 (control_tx, control_rx) = mpsc::channel();
control_tx
.send(ControlEvent::Connected { connection_id: 1 })
.expect("channel open");
let (tx, mut rx) = spsc::channel::<TxFrame>(8, WaitStrategy::SpinThenYield);
let (mut early_tx, early_rx) =
early_reply_channel(crate::early_reply::EARLY_REPLY_QUEUE_CAPACITY);
early_tx
.try_send(early(1, 0, kind))
.expect("queue has room");
early_tx
.try_send(early(1, 1, kind))
.expect("queue has room");
early_tx.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();
std::thread::scope(|scope| {
let stage = scope.spawn(|| {
run::<Counter>(
consumer,
control_rx,
DurableWireSeqCursor::detached(WireSeq::new(1)),
GatePolicy {
start: GateStart::settled(AckPolicy::DiskAndRam),
forced: forced_policy_cell().1,
},
Some(Arc::new(ReplicationMetrics::default())),
Some([
Arc::new(AtomicBool::new(false)),
Arc::new(AtomicBool::new(false)),
]),
&shutdown,
None,
Arc::new(AtomicU64::new(0)),
vec![tx],
Arc::new(StageUtilization::default()),
WaitStrategy::SpinThenYield,
Arc::new(ResponseEncoder),
early_rx,
on.then(|| DegradedRelease {
presence: Arc::new(ReplicaPresence::new()),
grace: Duration::ZERO,
}),
None,
)
});
let mut last_frame = Instant::now();
while last_frame.elapsed() < quiet {
match rx.try_consume() {
Some((_, frame)) => {
assert_eq!(frame.connection_id, 1);
wire_frames(frame.as_bytes(), &mut seen);
last_frame = Instant::now();
}
None => std::thread::yield_now(),
}
}
shutdown.store(true, Ordering::Relaxed);
stage.join().expect("stage panicked");
while let Some((_, frame)) = rx.try_consume() {
wire_frames(frame.as_bytes(), &mut seen);
}
});
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), REJECTED);
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), REJECTED);
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"
);
}
#[test]
fn every_kind_of_early_reply_goes_ahead_of_the_held_write_and_names_its_request() {
for kind in [
EarlyKind::Filtered,
EarlyKind::Refused {
kind: RefusalKind::PermissionDenied,
reason: "auditor role may not increment",
},
EarlyKind::Busy,
] {
let seen = run_halted(true, Duration::from_millis(500), kind);
let expected: Vec<Seen> = frames_of(kind, early_id(0))
.into_iter()
.chain(frames_of(kind, early_id(1)))
.chain([
Seen::App(KIND_RESP_ACK),
Seen::Final(TAG_BATCH_END_DEGRADED, HELD),
])
.collect();
assert_eq!(seen, expected, "{kind:?}");
}
}
}
mod early_release {
use super::*;
use std::collections::{HashMap, VecDeque};
use std::thread::JoinHandle;
use counter_server::{Counter, ResponseEncoder};
use melin_transport_core::lineage::forced_policy_cell;
use proptest::prelude::*;
use super::degraded_release::wire_frames;
use crate::ack_policy::AckPolicy;
use crate::durability_gate::GateStart;
use crate::response::tests::early_release::{
StageHarness, StageInputs, a_gate_held_inside_an_event_sends_no_early_reply,
an_early_reply_waits_out_an_event_split_across_reads,
connection_first_seen_inside_a_gate_wait, early_replies_overtake_a_held_write,
every_kind, every_request_is_answered_once_and_whole, script,
};
use crate::response::tests::refusal_order::Seen;
struct DpdkStage {
control: mpsc::Sender<ControlEvent>,
tx_ring: spsc::Consumer<TxFrame>,
pending: HashMap<u64, VecDeque<Seen>>,
shutdown: Arc<AtomicBool>,
handle: Option<JoinHandle<()>>,
}
fn start_dpdk(inputs: StageInputs) -> DpdkStage {
let (control, control_rx) = mpsc::channel();
let (tx, tx_ring) = spsc::channel::<TxFrame>(1024, WaitStrategy::SpinThenYield);
let shutdown = Arc::clone(&inputs.shutdown);
let StageInputs {
consumer,
early_replies,
journal,
replication_metrics,
replica_active,
utilization,
shutdown: stage_shutdown,
} = inputs;
let handle = std::thread::spawn(move || {
run::<Counter>(
consumer,
control_rx,
journal,
GatePolicy {
start: GateStart::settled(AckPolicy::DiskAndRam),
forced: forced_policy_cell().1,
},
Some(replication_metrics),
Some(replica_active),
&stage_shutdown,
None,
Arc::new(AtomicU64::new(0)),
vec![tx],
utilization,
WaitStrategy::SpinThenYield,
Arc::new(ResponseEncoder),
early_replies,
None,
None,
)
});
DpdkStage {
control,
tx_ring,
pending: HashMap::new(),
shutdown,
handle: Some(handle),
}
}
impl StageHarness for DpdkStage {
fn connect(&mut self, connection_id: u64) {
self.control
.send(ControlEvent::Connected { connection_id })
.expect("the stage is running");
}
fn next_frame(&mut self, connection_id: u64, within: Duration) -> Option<Seen> {
let deadline = Instant::now() + within;
loop {
if let Some(frame) = self
.pending
.get_mut(&connection_id)
.and_then(VecDeque::pop_front)
{
return Some(frame);
}
match self.tx_ring.try_consume() {
Some((_, frame)) => {
let mut seen = Vec::new();
wire_frames(frame.as_bytes(), &mut seen);
self.pending
.entry(frame.connection_id)
.or_default()
.extend(seen);
}
None if Instant::now() >= deadline => return None,
None => std::thread::yield_now(),
}
}
}
}
impl Drop for DpdkStage {
fn drop(&mut self) {
self.shutdown.store(true, Ordering::Relaxed);
if let Some(handle) = self.handle.take() {
if handle.join().is_err() && !std::thread::panicking() {
panic!("the stage panicked");
}
}
}
}
#[test]
fn an_early_reply_waits_out_an_event_split_across_reads_dpdk() {
an_early_reply_waits_out_an_event_split_across_reads(start_dpdk);
}
#[test]
fn early_replies_overtake_a_held_write_dpdk() {
for kind in every_kind() {
early_replies_overtake_a_held_write(start_dpdk, kind);
}
}
#[test]
fn a_gate_held_inside_an_event_sends_no_early_reply_dpdk() {
a_gate_held_inside_an_event_sends_no_early_reply(start_dpdk);
}
#[test]
fn connection_first_seen_inside_a_gate_wait_dpdk() {
connection_first_seen_inside_a_gate_wait(start_dpdk);
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(24))]
#[test]
fn every_request_is_answered_once_and_whole_dpdk(script in script()) {
every_request_is_answered_once_and_whole(start_dpdk, &script)?;
}
}
fn hook_turn(between_events: bool) -> (FxHashMap<u64, ConnectionHeartbeat>, Vec<Seen>) {
use crate::early_reply::{EarlyKind, early_reply_channel};
use crate::response::tests::refusal_order::early;
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 (producer, mut tx_ring) =
spsc::channel::<TxFrame>(1024, WaitStrategy::SpinThenYield);
let mut producers = vec![producer];
let mut connections = FxHashMap::default();
control_tx
.send(ControlEvent::Connected { connection_id: 1 })
.expect("the receiver is held");
tx.try_send(early(1, 0, EarlyKind::Busy))
.expect("queue has room");
tx.flush();
let boundary = BatchBoundary::new();
let mut early_buf: EarlyReplyBuf = [0u8; MAX_EARLY_REPLY];
GateWaitHook {
at_boundary: if between_events {
Some(boundary.at_boundary().expect("between events"))
} else {
None
},
registered: &mut registered,
control_rx: &control_rx,
connections: &mut connections,
active_connections: &AtomicU64::new(1),
tx_producers: &mut producers,
encoder: &ResponseEncoder,
early_buf: &mut early_buf,
}
.while_waiting();
let mut seen = Vec::new();
while let Some((_, frame)) = tx_ring.try_consume() {
assert_eq!(frame.connection_id, 1);
wire_frames(frame.as_bytes(), &mut seen);
}
(connections, seen)
}
#[test]
fn gate_wait_hook_between_events_registers_and_sends() {
use crate::early_reply::EarlyKind;
use crate::response::tests::refusal_order::{early_id, frames_of};
let (connections, seen) = hook_turn(true);
assert!(connections.contains_key(&1), "not registered");
assert_eq!(seen, frames_of(EarlyKind::Busy, early_id(0)));
}
#[test]
fn gate_wait_hook_inside_an_event_does_nothing() {
let (connections, seen) = hook_turn(false);
assert!(connections.is_empty(), "control drained");
assert!(seen.is_empty(), "a reply went out: {seen:?}");
}
}
mod push_frame {
use super::*;
const TRAILER: &[u8] = b"END";
fn pushed(
payload: Option<Result<usize, &'static str>>,
trailer: &[u8],
) -> Vec<(u64, Vec<u8>)> {
let (mut tx, mut rx) = spsc::channel::<TxFrame>(8, WaitStrategy::SpinThenYield);
let encode_buf = b"response-body";
super::super::push_frame(payload, 42, &mut tx, encode_buf, trailer);
tx.flush();
let mut out = Vec::new();
while let Some((_, frame)) = rx.try_consume() {
out.push((frame.connection_id, frame.as_bytes().to_vec()));
}
out
}
#[test]
fn payload_and_trailer_ship_as_one_frame() {
let frames = pushed(Some(Ok(8)), TRAILER);
assert_eq!(frames.len(), 1, "two SPSC slots where one suffices");
assert_eq!(frames[0].0, 42);
assert_eq!(frames[0].1, b"responseEND".to_vec());
}
#[test]
fn payload_without_trailer_ships_alone() {
let frames = pushed(Some(Ok(8)), &[]);
assert_eq!(frames.len(), 1);
assert_eq!(frames[0].1, b"response".to_vec());
}
#[test]
fn trailer_without_payload_ships_alone() {
let frames = pushed(None, TRAILER);
assert_eq!(frames.len(), 1);
assert_eq!(frames[0].1, TRAILER.to_vec());
}
#[test]
fn nothing_to_send_pushes_no_frame() {
assert!(pushed(None, &[]).is_empty());
}
#[test]
fn encode_failure_still_terminates_the_request() {
let frames = pushed(Some(Err("encode error")), TRAILER);
assert_eq!(frames.len(), 1);
assert_eq!(frames[0].1, TRAILER.to_vec());
}
#[test]
fn encode_failure_without_trailer_pushes_no_frame() {
assert!(pushed(Some(Err("encode error")), &[]).is_empty());
}
#[test]
fn the_encoded_terminator_fits_its_reserved_room() {
let id = melin_transport_core::pipeline::RequestId(u64::MAX);
for terminator in [
TransportResponse::BatchEnd { id },
TransportResponse::BatchEndDegraded { id },
] {
let mut buf = [0u8; BATCH_END_FRAME_LEN];
let written = control_codec::encode_transport_response(&terminator, &mut buf)
.expect("a terminator encodes into its reserved room");
assert_eq!(written, BATCH_END_FRAME_LEN);
}
assert_eq!(MAX_TX_FRAME, MAX_APP_FRAME + BATCH_END_FRAME_LEN);
}
#[test]
fn the_widest_response_and_its_terminator_fit_one_frame() {
let response = [0xAB; MAX_APP_FRAME];
let mut trailer = [0u8; BATCH_END_FRAME_LEN];
let written = control_codec::encode_transport_response(
&TransportResponse::BatchEnd {
id: melin_transport_core::pipeline::RequestId(9),
},
&mut trailer,
)
.expect("a terminator encodes");
assert_eq!(written, BATCH_END_FRAME_LEN);
let (mut tx, mut rx) = spsc::channel::<TxFrame>(8, WaitStrategy::SpinThenYield);
super::super::push_frame(Some(Ok(MAX_APP_FRAME)), 42, &mut tx, &response, &trailer);
tx.flush();
let (_, frame) = rx.try_consume().expect("one frame");
assert_eq!(frame.len as usize, MAX_TX_FRAME);
assert_eq!(frame.as_bytes(), [&response[..], &trailer].concat());
assert!(rx.try_consume().is_none());
}
#[test]
#[should_panic(expected = "overrun")]
fn a_frame_past_a_tx_slot_panics_by_name() {
let response = [0xAB; MAX_APP_FRAME];
let trailer = [0u8; BATCH_END_FRAME_LEN + 1];
let (mut tx, _rx) = spsc::channel::<TxFrame>(8, WaitStrategy::SpinThenYield);
super::super::push_frame(Some(Ok(MAX_APP_FRAME)), 42, &mut tx, &response, &trailer);
}
}
}