use std::io::{Read, Write};
use std::os::unix::io::AsRawFd;
use std::os::unix::net::UnixStream;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, mpsc};
use std::thread;
use std::time::Duration;
use crate::ControlEvent;
use crate::ack_policy::AckPolicy;
use crate::connection_limit::RingSizing;
use crate::early_reply;
use crate::response::{self, Response};
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;
const FRAME_LEN: usize = 14;
const PAYLOAD_LEN: u32 = 10;
const READ_TIMEOUT: Duration = Duration::from_secs(5);
const SETTLE: Duration = Duration::from_millis(50);
fn ack_slot(wire_seq: u64, new_value: u64) -> OutputSlot<CounterReport, CounterQuery> {
OutputSlot {
connection_id: 1,
wire_seq,
payload: OutputPayload::Report(CounterReport::Ack { new_value }),
is_last_in_request: false,
..Default::default()
}
}
fn read_ack(sock: &mut UnixStream) -> std::io::Result<u64> {
let mut frame = [0u8; FRAME_LEN];
sock.read_exact(&mut frame)?;
assert_eq!(
u32::from_le_bytes(frame[..4].try_into().expect("4 bytes")),
PAYLOAD_LEN,
"unexpected length prefix"
);
assert_eq!(
frame[4..6],
[TAG_APP, KIND_RESP_ACK],
"an application frame carrying the counter's ack"
);
Ok(u64::from_le_bytes(frame[6..].try_into().expect("8 bytes")))
}
fn config_for(journal_cursor: DurableWireSeqCursor) -> Response<Counter> {
Response::<Counter> {
journal_persisted_wire_seq: journal_cursor,
policy: response::GatePolicy {
start: crate::durability_gate::GateStart::settled(AckPolicy::Disk),
forced: melin_transport_core::lineage::forced_policy_cell().1,
},
replication_metrics: None,
replica_active: None,
heartbeat_interval: None,
wait: WaitStrategy::SpinThenYield,
utilization: Arc::new(StageUtilization::default()),
encoder: Arc::new(ResponseEncoder),
fence_state: Arc::new(FenceState::new(0)),
active_connections: Arc::new(std::sync::atomic::AtomicU64::new(0)),
early_replies: early_reply::early_reply_channel(early_reply::EARLY_REPLY_QUEUE_CAPACITY).1,
ring_sizing: RingSizing::for_max_connections(1).expect("a supported cap"),
ready: None,
degraded_release: None,
pause_after_control_drain: None,
}
}
#[test]
fn durable_response_is_released_before_blocking_on_a_later_gate() {
let (mut producer, mut consumers) =
DisruptorBuilder::<OutputSlot<CounterReport, CounterQuery>>::new(1024)
.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 journal_cursor = DurableWireSeqCursor::detached(WireSeq::new(0));
let shutdown = AtomicBool::new(false);
let (control_tx, control_rx) = mpsc::channel();
let config = config_for(journal_cursor.clone());
thread::scope(|scope| {
let stage = scope.spawn(|| {
response::run::<Counter>(consumer, control_rx, config, &shutdown);
});
control_tx
.send(ControlEvent::Connected {
connection_id: 1,
fd: server_fd,
writer,
})
.expect("stage is running");
producer.publish(ack_slot(1, 111));
thread::sleep(SETTLE);
producer.publish(ack_slot(2, 222));
thread::sleep(SETTLE);
journal_cursor.store(WireSeq::new(1));
let first = read_ack(&mut client_sock);
journal_cursor.store(WireSeq::new(2));
let second = read_ack(&mut client_sock);
shutdown.store(true, Ordering::Relaxed);
stage.join().expect("response stage panicked");
let first = first
.expect("durable response withheld while the stage waited on a later event's gate");
assert_eq!(first, 111, "wrong response released first");
let second = second.expect("second response never arrived");
assert_eq!(second, 222, "responses delivered out of order");
});
}
#[test]
fn earlier_slot_in_a_batch_is_not_held_by_a_later_one() {
let (mut producer, mut consumers) =
DisruptorBuilder::<OutputSlot<CounterReport, CounterQuery>>::new(1024)
.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 journal_cursor = DurableWireSeqCursor::detached(WireSeq::new(0));
let shutdown = AtomicBool::new(false);
let (control_tx, control_rx) = mpsc::channel();
let config = config_for(journal_cursor.clone());
thread::scope(|scope| {
let stage = scope.spawn(|| {
response::run::<Counter>(consumer, control_rx, config, &shutdown);
});
control_tx
.send(ControlEvent::Connected {
connection_id: 1,
fd: server_fd,
writer,
})
.expect("stage is running");
producer.publish(ack_slot(1, 111));
thread::sleep(SETTLE);
producer.publish(ack_slot(2, 222));
producer.publish(ack_slot(3, 333));
thread::sleep(SETTLE);
journal_cursor.store(WireSeq::new(2));
let held = read_ack(&mut client_sock);
let early = read_ack(&mut client_sock);
journal_cursor.store(WireSeq::new(3));
let late = read_ack(&mut client_sock);
shutdown.store(true, Ordering::Relaxed);
stage.join().expect("response stage panicked");
assert_eq!(held.expect("held response never arrived"), 111);
assert_eq!(
early.expect("durable slot held behind a later slot in the same batch"),
222
);
assert_eq!(late.expect("last response never arrived"), 333);
});
}
#[test]
fn open_gate_delivers_the_whole_batch_in_order() {
let (mut producer, mut consumers) =
DisruptorBuilder::<OutputSlot<CounterReport, CounterQuery>>::new(1024)
.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 journal_cursor = DurableWireSeqCursor::detached(WireSeq::new(100));
let shutdown = AtomicBool::new(false);
let (control_tx, control_rx) = mpsc::channel();
let config = config_for(journal_cursor);
thread::scope(|scope| {
let stage = scope.spawn(|| {
response::run::<Counter>(consumer, control_rx, config, &shutdown);
});
control_tx
.send(ControlEvent::Connected {
connection_id: 1,
fd: server_fd,
writer,
})
.expect("stage is running");
for (seq, value) in [(1u64, 11u64), (2, 22), (3, 33)] {
producer.publish(ack_slot(seq, value));
}
let received: Vec<_> = (0..3).map(|_| read_ack(&mut client_sock)).collect();
shutdown.store(true, Ordering::Relaxed);
stage.join().expect("response stage panicked");
for (value, expected) in received.into_iter().zip([11u64, 22, 33]) {
let value = value.expect("response never arrived");
assert_eq!(value, expected, "responses delivered out of order");
}
});
}
#[test]
fn sustained_busy_stream_is_delivered_not_disconnected() {
const SLOTS: u64 = 8192;
let (mut producer, mut consumers) =
DisruptorBuilder::<OutputSlot<CounterReport, CounterQuery>>::new(SLOTS as usize)
.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 journal_cursor = DurableWireSeqCursor::detached(WireSeq::new(0));
let shutdown = AtomicBool::new(false);
let (control_tx, control_rx) = mpsc::channel();
let config = config_for(journal_cursor.clone());
thread::scope(|scope| {
let stage = scope.spawn(|| {
response::run::<Counter>(consumer, control_rx, config, &shutdown);
});
control_tx
.send(ControlEvent::Connected {
connection_id: 1,
fd: server_fd,
writer,
})
.expect("stage is running");
for i in 1..=SLOTS {
producer.publish(ack_slot(i, i));
}
journal_cursor.store(WireSeq::new(SLOTS));
let mut delivered = 0u64;
let mut failure: Option<String> = None;
for i in 1..=SLOTS {
match read_ack(&mut client_sock) {
Ok(value) if value == i => delivered += 1,
Ok(value) => {
failure = Some(format!("out of order: got {value}, expected {i}"));
break;
}
Err(e) => {
failure = Some(format!(
"response {i} of {SLOTS} never arrived ({e}) — busy-path flush missing"
));
break;
}
}
}
shutdown.store(true, Ordering::Relaxed);
stage.join().expect("response stage panicked");
if let Some(failure) = failure {
panic!("{failure}");
}
assert_eq!(delivered, SLOTS, "every response delivered");
});
}