use std::net::SocketAddr;
use std::path::Path;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Duration, Instant};
use counter_server::{
Counter, CounterEvent, CounterQuery, CounterReport, CounterRole, GET_VALUE_REQUEST,
KIND_RESP_ACK, KIND_RESP_VALUE, RequestDecoder, ResponseEncoder, increment_request,
};
use melin_app::auth::ClientRole;
use melin_app::decoder::Decoded;
use melin_app::{Application, ApplyCtx, QueryCtx, RejectReason};
use melin_client::{Connection, Frame, RefusalKind, RequestId, SigningKey, StreamConnection, key};
use melin_server_runtime::StartupEvents;
use melin_server_runtime::layout::PipelineCores;
use melin_server_runtime::server::ServerConfig;
use melin_test_node::probe::wait_for_gauge;
use melin_transport_core::pipeline::INPUT_RING_CAPACITY;
use melin_transport_core::test_ports::free_addr;
const WRITER: [u8; 32] = [0xA1; 32];
const READER: [u8; 32] = [0xA2; 32];
const FILTERED: [u8; 1] = [0x7F];
const MALFORMED: [u8; 1] = [0x55];
struct FilteringDecoder;
impl melin_app::decoder::RequestDecoder for FilteringDecoder {
type Event = CounterEvent;
type Role = CounterRole;
fn decode(&self, body: &[u8], role: ClientRole<CounterRole>) -> Decoded<CounterEvent> {
if body == FILTERED {
return Decoded::Filter;
}
RequestDecoder.decode(body, role)
}
}
#[derive(Default)]
struct Latched {
counter: Counter,
leave: Option<Arc<AtomicU64>>,
applied: u64,
}
impl Application for Latched {
type Event = CounterEvent;
type Report = CounterReport;
type QueryResponse = CounterQuery;
type Sizing = Arc<AtomicU64>;
const APP_VERSION: u16 = Counter::APP_VERSION;
fn apply(&mut self, event: Self::Event, ctx: &ApplyCtx, out: &mut Vec<Self::Report>) {
while self
.leave
.as_ref()
.is_some_and(|leave| self.applied >= leave.load(Ordering::Acquire))
{
std::thread::sleep(Duration::from_micros(50));
}
self.applied += 1;
self.counter.apply(event, ctx, out)
}
fn query(&self, event: Self::Event, ctx: &QueryCtx) -> Option<Self::QueryResponse> {
self.counter.query(event, ctx)
}
fn tick(&mut self, now_ns: u64, out: &mut Vec<Self::Report>) {
self.counter.tick(now_ns, out)
}
fn build_reject(event: &Self::Event, reason: RejectReason) -> Self::Report {
Counter::build_reject(event, reason)
}
fn snapshot<W: std::io::Write>(&self, w: &mut W) -> std::io::Result<()> {
self.counter.snapshot(w)
}
fn restore<R: std::io::Read>(r: &mut R) -> std::io::Result<Self> {
Counter::restore(r).map(|counter| Latched {
counter,
..Latched::default()
})
}
fn prefault(&mut self, leave: &Self::Sizing) {
self.leave = Some(Arc::clone(leave));
}
}
fn authorized_keys(dir: &Path) -> std::path::PathBuf {
let auth_path = dir.join("authorized_keys");
let line = |role: &str, secret: [u8; 32]| {
key::authorized_keys_line(role, &SigningKey::from_bytes(&secret).verifying_key(), role)
};
std::fs::write(
&auth_path,
format!("{}\n{}\n", line("writer", WRITER), line("reader", READER)),
)
.expect("write auth keys");
auth_path
}
const PORT_BASE: u16 = 10_000;
fn config(dir: &Path, name: &str) -> ServerConfig {
ServerConfig {
journal: dir.join(name),
authorized_keys: authorized_keys(dir),
standalone: true,
ack_policy: melin_server_runtime::ack_policy::AckPolicy::Disk,
cores: PipelineCores::unpinned(),
tick_interval_ms: 0,
snapshot_interval_ms: 0,
health_bind: Some(free_addr(PORT_BASE)),
..melin_test_node::config()
}
}
fn start(dir: &Path) -> (melin_test_node::Node, SocketAddr) {
let config = config(dir, "counter.journal");
let health = config.health_bind.expect("set by `config`");
let node = melin_test_node::start::<Counter>(
config,
StartupEvents {
genesis: Vec::new(),
on_primary: Vec::new(),
},
(),
FilteringDecoder,
ResponseEncoder,
);
(node, health)
}
fn start_latched(dir: &Path, leave: &Arc<AtomicU64>) -> (melin_test_node::Node, SocketAddr) {
let config = config(dir, "latched.journal");
let health = config.health_bind.expect("set by `config`");
let node = melin_test_node::start::<Latched>(
config,
StartupEvents {
genesis: Vec::new(),
on_primary: Vec::new(),
},
Arc::clone(leave),
RequestDecoder,
ResponseEncoder,
);
(node, health)
}
const EARLY_ANSWER_COUNTERS: [&str; 5] = [
"melin_requests_shed_total",
"melin_requests_refused_total{kind=\"permission_denied\"}",
"melin_requests_refused_total{kind=\"malformed\"}",
"melin_requests_filtered_total",
"melin_connections_parked",
];
fn wait_for_counters(health: SocketAddr, expected: [u64; 5]) {
for (name, value) in EARLY_ANSWER_COUNTERS.into_iter().zip(expected) {
wait_for_gauge(health, name, value);
}
}
#[derive(Debug, PartialEq, Eq)]
enum Answer {
Batch(Vec<(u8, u64)>),
Refused(RefusalKind, String),
Busy,
}
fn assert_answered(sent: &[RequestId], answers: &[(RequestId, Answer)], expected: Vec<Answer>) {
let wanted: Vec<(RequestId, Answer)> = sent.iter().copied().zip(expected).collect();
fn by_id(list: &[(RequestId, Answer)]) -> Vec<&(RequestId, Answer)> {
let mut list: Vec<_> = list.iter().collect();
list.sort_by_key(|(id, _)| *id);
list
}
assert_eq!(
by_id(answers),
by_id(&wanted),
"every request answered once, naming it"
);
let accepted = |list: &[(RequestId, Answer)]| -> Vec<RequestId> {
list.iter()
.filter(|(_, answer)| matches!(answer, Answer::Batch(frames) if !frames.is_empty()))
.map(|(id, _)| *id)
.collect()
};
assert_eq!(
accepted(answers),
accepted(&wanted),
"the accepted requests answered in request order"
);
}
fn pipelined(
conn: &mut Connection,
bodies: &[&[u8]],
) -> (Vec<RequestId>, Vec<(RequestId, Answer)>) {
let sent: Vec<RequestId> = bodies
.iter()
.map(|body| conn.send(body).expect("send"))
.collect();
let mut answers = Vec::new();
let mut frames = Vec::new();
while answers.len() < bodies.len() {
match conn.next_frame().expect("every request is answered") {
Frame::Response(body) => frames.push((
body[0],
u64::from_le_bytes(body[1..9].try_into().expect("an 8-byte value")),
)),
Frame::BatchEnd { id, .. } => {
answers.push((id, Answer::Batch(std::mem::take(&mut frames))));
}
Frame::Refused { id, kind, reason } => {
assert!(frames.is_empty(), "a refusal inside a batch");
answers.push((id, Answer::Refused(kind, reason.to_string())));
}
Frame::ServerBusy { id } => answers.push((id, Answer::Busy)),
}
}
(sent, answers)
}
#[test]
fn every_pipelined_request_is_answered_and_paired_by_id() {
let dir = tempfile::tempdir().expect("tempdir");
let (node, health) = start(dir.path());
let mut writer = melin_test_node::connect(node.addr(), &SigningKey::from_bytes(&WRITER));
writer
.set_read_timeout(Duration::from_secs(30))
.expect("set the read timeout");
let (sent, answers) = pipelined(
&mut writer,
&[
&increment_request(1),
&MALFORMED,
&increment_request(2),
&FILTERED,
&increment_request(4),
&GET_VALUE_REQUEST,
],
);
let malformed = Answer::Refused(RefusalKind::Malformed, "unknown kind".into());
let expected = vec![
Answer::Batch(vec![(KIND_RESP_ACK, 1)]),
malformed,
Answer::Batch(vec![(KIND_RESP_ACK, 3)]),
Answer::Batch(vec![]),
Answer::Batch(vec![(KIND_RESP_ACK, 7)]),
Answer::Batch(vec![(KIND_RESP_VALUE, 7)]),
];
assert_answered(&sent, &answers, expected);
let mut reader = melin_test_node::connect(node.addr(), &SigningKey::from_bytes(&READER));
reader
.set_read_timeout(Duration::from_secs(30))
.expect("set the read timeout");
let (sent, answers) = pipelined(
&mut reader,
&[
&GET_VALUE_REQUEST,
&increment_request(100),
&GET_VALUE_REQUEST,
],
);
let denied = Answer::Refused(
RefusalKind::PermissionDenied,
"incrementing requires a writing role".into(),
);
let expected = vec![
Answer::Batch(vec![(KIND_RESP_VALUE, 7)]),
denied,
Answer::Batch(vec![(KIND_RESP_VALUE, 7)]),
];
assert_answered(&sent, &answers, expected);
assert_eq!(sent[0], RequestId(1));
let err = reader.request(&MALFORMED).unwrap_err();
assert!(
matches!(&err, melin_client::Error::Refused { kind: RefusalKind::Malformed, reason } if reason == "unknown kind"),
"{err:?}"
);
assert_eq!(
writer.request(&FILTERED).expect("an empty batch"),
Vec::<Vec<u8>>::new()
);
let value = writer.request_one(&GET_VALUE_REQUEST).expect("the query");
assert_eq!(value[0], KIND_RESP_VALUE);
assert_eq!(
u64::from_le_bytes(value[1..9].try_into().expect("8 bytes")),
7
);
wait_for_counters(health, [0, 1, 2, 2, 0]);
drop((writer, reader));
node.stop();
}
#[test]
fn a_full_input_ring_answers_busy_and_keeps_the_connection() {
const PAST_THE_RING: usize = 20_000;
const SETTLED: Duration = Duration::from_secs(2);
const WINDOW: u64 = 1024;
melin_test_node::logs::capture_if_requested();
let dir = tempfile::tempdir().expect("tempdir");
let leave = Arc::new(AtomicU64::new(0));
let (node, health) = start_latched(dir.path(), &leave);
let key = SigningKey::from_bytes(&WRITER);
drop(melin_test_node::connect(node.addr(), &key));
let mut conn = StreamConnection::connect_timeout(node.addr(), &key, Duration::from_secs(60))
.expect("the client connects");
let total = INPUT_RING_CAPACITY + PAST_THE_RING;
let body = increment_request(1);
let mut busy: Vec<RequestId> = Vec::new();
let mut applied: Vec<RequestId> = Vec::new();
fn read_answers(
conn: &mut StreamConnection<melin_client::NonBlockingTcp>,
busy: &mut Vec<RequestId>,
applied: &mut Vec<RequestId>,
) {
while let Some(frame) = conn.poll().expect("the connection stays up") {
match frame {
Frame::ServerBusy { id } => busy.push(id),
Frame::BatchEnd { id, .. } => applied.push(id),
Frame::Response(_) => {}
Frame::Refused { id, kind, reason } => {
panic!("request {id:?} refused ({kind:?}: {reason})")
}
}
}
}
let mut sent = Vec::with_capacity(total);
for n in 0..total {
sent.push(conn.send(&body).expect("send"));
if n % 256 == 255 {
read_answers(&mut conn, &mut busy, &mut applied);
}
}
let mut last_change = (busy.len(), Instant::now());
while conn.connection().unsent() > 0 || last_change.1.elapsed() < SETTLED {
read_answers(&mut conn, &mut busy, &mut applied);
if busy.len() != last_change.0 {
last_change = (busy.len(), Instant::now());
}
std::thread::sleep(Duration::from_millis(1));
}
assert!(
applied.is_empty(),
"nothing applied while the stage is held"
);
assert!(
busy.len() >= PAST_THE_RING / 2,
"the requests past the ring are shed: {} of {total}",
busy.len()
);
while busy.len() + applied.len() < total {
leave.store(applied.len() as u64 + WINDOW, Ordering::Release);
read_answers(&mut conn, &mut busy, &mut applied);
std::thread::yield_now();
}
leave.store(u64::MAX, Ordering::Release);
assert_eq!(busy.len() + applied.len(), total);
assert!(applied.windows(2).all(|w| w[0] < w[1]), "in request order");
let last_applied = *applied.last().expect("the ring's requests are applied");
let first_busy = *busy.iter().min().expect("some are shed");
assert!(
last_applied < first_busy,
"applied {last_applied:?} after shedding {first_busy:?}"
);
assert_eq!(*sent.last().expect("sent"), RequestId(total as u64));
let ack = conn
.request_one(&increment_request(5))
.expect("the next request");
assert_eq!(ack[0], KIND_RESP_ACK);
assert_eq!(
u64::from_le_bytes(ack[1..9].try_into().expect("8 bytes")),
applied.len() as u64 + 5
);
wait_for_counters(health, [busy.len() as u64, 0, 0, 0, 0]);
drop(conn);
node.stop();
}