use std::net::SocketAddr;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use base64::Engine;
use counter_server::{
Counter, CounterEvent, CounterQuery, CounterReport, GET_VALUE_REQUEST, KIND_RESP_ACK,
KIND_RESP_VALUE, RequestDecoder, ResponseEncoder, increment_request,
};
use ed25519_dalek::SigningKey;
use melin_app::{Application, ApplyCtx, QueryCtx, RejectReason};
use melin_client::Connection;
use melin_server_runtime::StartupEvents;
use melin_server_runtime::ack_policy::AckPolicy;
use melin_server_runtime::layout::PipelineCores;
use melin_server_runtime::server::ServerConfig;
use melin_test_node::probe::wait_for_gauge;
use melin_test_node::{Addrs, Node};
use melin_transport_core::test_ports::free_addr;
const PORT_BASE: u16 = 10_000;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Sized {
reserve_for: u64,
value: u64,
}
#[derive(Clone)]
struct Sizing {
reserve_for: u64,
log: Arc<Mutex<Vec<Sized>>>,
}
impl Sizing {
fn new(reserve_for: u64) -> Self {
Sizing {
reserve_for,
log: Arc::new(Mutex::new(Vec::new())),
}
}
fn calls(&self) -> Vec<Sized> {
self.log.lock().expect("probe log").clone()
}
}
#[derive(Default)]
struct SizedCounter(Counter);
impl SizedCounter {
fn value(&self) -> u64 {
let mut buf = Vec::new();
self.0.snapshot(&mut buf).expect("snapshot to a vec");
u64::from_le_bytes(buf[..8].try_into().expect("8-byte value"))
}
}
impl Application for SizedCounter {
type Event = CounterEvent;
type Report = CounterReport;
type QueryResponse = CounterQuery;
type Sizing = Sizing;
const APP_VERSION: u16 = Counter::APP_VERSION;
fn apply(&mut self, event: Self::Event, ctx: &ApplyCtx, out: &mut Vec<Self::Report>) {
self.0.apply(event, ctx, out)
}
fn query(&self, event: Self::Event, ctx: &QueryCtx) -> Option<Self::QueryResponse> {
self.0.query(event, ctx)
}
fn tick(&mut self, now_ns: u64, out: &mut Vec<Self::Report>) {
self.0.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.0.snapshot(w)
}
fn restore<R: std::io::Read>(r: &mut R) -> std::io::Result<Self> {
Counter::restore(r).map(SizedCounter)
}
fn prefault(&mut self, sizing: &Self::Sizing) {
sizing.log.lock().expect("probe log").push(Sized {
reserve_for: sizing.reserve_for,
value: self.value(),
});
}
}
fn spawn_node(
addrs: &Addrs,
config: ServerConfig,
startup: StartupEvents<CounterEvent>,
sizing: &Sizing,
) -> Node {
melin_test_node::start_at::<SizedCounter>(
addrs,
config,
startup,
sizing.clone(),
RequestDecoder,
ResponseEncoder,
)
}
fn connect(addr: SocketAddr, key: &SigningKey) -> Connection {
let deadline = Instant::now() + Duration::from_secs(30);
let mut conn = Connection::connect_by(addr, key, deadline).expect("client connects");
conn.set_read_timeout(Duration::from_secs(30))
.expect("set timeout");
conn
}
fn value_of(conn: &mut Connection) -> u64 {
let reply = conn.request_one(&GET_VALUE_REQUEST).expect("query");
assert_eq!(reply[0], KIND_RESP_VALUE);
u64::from_le_bytes(reply[1..9].try_into().expect("8 bytes"))
}
fn increment(conn: &mut Connection, amount: u64) {
let reply = conn
.request_one(&increment_request(amount))
.expect("increment");
assert_eq!(reply[0], KIND_RESP_ACK, "the increment is acked");
}
const GENESIS: u64 = 1_000;
const PRIMARY_RESERVE: u64 = 4_096;
const REPLICA_RESERVE: u64 = 512;
const RESTART_RESERVE: u64 = 65_536;
#[test]
fn every_node_sizes_its_own_instances_before_serving() {
melin_test_node::logs::capture_if_requested();
let tmp = tempfile::tempdir().expect("tempdir");
let primary_key = SigningKey::from_bytes(&[0x81; 32]);
let replica_key = SigningKey::from_bytes(&[0x82; 32]);
let client_key = SigningKey::from_bytes(&[0x83; 32]);
let b64 = |k: &SigningKey| {
base64::engine::general_purpose::STANDARD.encode(k.verifying_key().to_bytes())
};
let auth_path = tmp.path().join("authorized_keys");
std::fs::write(
&auth_path,
format!(
"replication {} primary\nreplication {} replica\noperator {} client\n",
b64(&primary_key),
b64(&replica_key),
b64(&client_key)
),
)
.expect("write authorized_keys");
let node_config = |name: &str, key: &SigningKey| -> ServerConfig {
let key_path = tmp.path().join(format!("{name}.key"));
std::fs::write(&key_path, key.to_bytes()).expect("write node key");
ServerConfig {
journal: tmp.path().join(format!("{name}.journal")),
authorized_keys: auth_path.clone(),
ack_policy: AckPolicy::Disk,
cores: PipelineCores::unpinned(),
tick_interval_ms: 0,
snapshot_interval_ms: 0,
health_bind: Some(free_addr(PORT_BASE)),
replication_key: Some(key_path),
..melin_test_node::config()
}
};
let genesis = || StartupEvents {
genesis: vec![CounterEvent::Increment { amount: GENESIS }],
on_primary: Vec::new(),
};
let primary_addrs = melin_test_node::addrs(0, || free_addr(PORT_BASE));
let replica_addrs = melin_test_node::addrs(1, || free_addr(PORT_BASE));
let mut primary_config = node_config("primary", &primary_key);
primary_config.replication_bind = Some(primary_addrs.replication());
let primary_health = primary_config.health_bind.expect("set above");
let mut replica_config = node_config("replica", &replica_key);
replica_config.replica_of = Some(primary_addrs.replication());
let primary_sizing = Sizing::new(PRIMARY_RESERVE);
let primary = spawn_node(&primary_addrs, primary_config, genesis(), &primary_sizing);
let replica_sizing = Sizing::new(REPLICA_RESERVE);
let replica = spawn_node(&replica_addrs, replica_config, genesis(), &replica_sizing);
wait_for_gauge(primary_health, "melin_replicas_connected", 1);
let mut conn = connect(primary.addr(), &client_key);
assert_eq!(value_of(&mut conn), GENESIS, "genesis applied");
assert_eq!(
primary_sizing.calls(),
vec![
Sized {
reserve_for: PRIMARY_RESERVE,
value: 0,
},
Sized {
reserve_for: PRIMARY_RESERVE,
value: GENESIS,
},
],
"a new primary is sized before its genesis is applied and again before it serves"
);
increment(&mut conn, 5);
wait_for_gauge(
primary_health,
"melin_replica_acked_sequence{slot=\"0\"}",
3,
);
assert_eq!(
replica_sizing.calls(),
vec![Sized {
reserve_for: REPLICA_RESERVE,
value: 0,
}],
"the replica is sized with its own sizing, before it applies the stream"
);
drop(conn);
replica.stop();
primary.stop();
assert_eq!(
primary_sizing.calls().len(),
2,
"nothing sizes the primary again while it serves"
);
let primary_addrs = melin_test_node::addrs(0, || free_addr(PORT_BASE));
let replica_addrs = melin_test_node::addrs(1, || free_addr(PORT_BASE));
let mut primary_config = node_config("primary", &primary_key);
primary_config.replication_bind = Some(primary_addrs.replication());
let primary_health = primary_config.health_bind.expect("set above");
let mut replica_config = node_config("replica", &replica_key);
replica_config.replica_of = Some(primary_addrs.replication());
let primary_sizing = Sizing::new(RESTART_RESERVE);
let primary = spawn_node(&primary_addrs, primary_config, genesis(), &primary_sizing);
let replica_sizing = Sizing::new(RESTART_RESERVE + 1);
let replica = spawn_node(&replica_addrs, replica_config, genesis(), &replica_sizing);
wait_for_gauge(primary_health, "melin_replicas_connected", 1);
let mut conn = connect(primary.addr(), &client_key);
let replayed = value_of(&mut conn);
drop(conn);
replica.stop();
primary.stop();
assert_eq!(replayed, GENESIS + 5, "the journal replayed");
let recovered = |reserve_for: u64| {
vec![
Sized {
reserve_for,
value: 0,
},
Sized {
reserve_for,
value: GENESIS + 5,
},
]
};
assert_eq!(
primary_sizing.calls(),
recovered(RESTART_RESERVE),
"a recovering primary is sized before the replay and on the recovered state, \
with the sizing it was restarted with"
);
assert_eq!(
replica_sizing.calls(),
recovered(RESTART_RESERVE + 1),
"a recovering replica is sized before the replay and on the recovered state, \
with the sizing it was restarted with"
);
}