use std::net::{IpAddr, SocketAddr};
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
use reconcile::{
replicated_map::Config, InMemoryNetwork, InMemoryTransport, ReplicatedMap, Transport,
};
#[path = "../benches/netem/mod.rs"]
mod netem;
use netem::{Impairments, Link, Netem, NetemTransport, Probability, Rtt, Seed};
const PATIENCE: Duration = Duration::from_secs(5);
fn fresh_addr(last: u8, port: u16) -> SocketAddr {
static BLOCK: AtomicU32 = AtomicU32::new(1);
let block = BLOCK.fetch_add(1, Ordering::Relaxed);
let ip: IpAddr = format!("127.4.{}.{last}", block % 256).parse().unwrap();
SocketAddr::new(ip, port)
}
struct Fixture {
network: InMemoryNetwork,
sender: NetemTransport<InMemoryTransport>,
receiver: InMemoryTransport,
destination: SocketAddr,
}
impl Fixture {
fn new(netem: Netem, port: u16) -> Fixture {
Fixture::between(netem, fresh_addr(1, port), fresh_addr(2, port))
}
fn between(netem: Netem, source: SocketAddr, destination: SocketAddr) -> Fixture {
let network = InMemoryNetwork::new();
let sender = NetemTransport::new(Arc::new(network.bind(source)), netem);
let receiver = network.bind(destination);
Fixture {
network,
sender,
receiver,
destination,
}
}
async fn send(&self, bytes: &[u8]) {
self.sender.send_to(bytes, &self.destination).await.unwrap();
}
async fn recv(&self) -> Vec<u8> {
let mut buf = [0u8; 64];
let (n, _) = tokio::time::timeout(PATIENCE, self.receiver.recv_from(&mut buf))
.await
.expect("a datagram the model did not drop must arrive")
.expect("in-memory receive cannot fail");
buf[..n].to_vec()
}
}
#[test]
fn rtt_halves_into_a_one_way_delay() {
assert_eq!(Rtt::from_millis(50.0).one_way(), Duration::from_millis(25));
assert_eq!(Rtt::from_millis(0.1).one_way(), Duration::from_micros(50));
assert_eq!(Rtt::ZERO.one_way(), Duration::ZERO);
}
#[test]
fn labels_are_integer_arithmetic_not_float_formatting() {
assert_eq!(Rtt::ZERO.label(), "rtt=0ms");
assert_eq!(Rtt::from_millis(0.1).label(), "rtt=0.1ms");
assert_eq!(Rtt::from_millis(1.0).label(), "rtt=1ms");
assert_eq!(Rtt::from_millis(50.0).label(), "rtt=50ms");
assert_eq!(Probability::percent(0.1).label(), "loss=0.1%");
assert_eq!(Probability::percent(1.0).label(), "loss=1%");
}
#[test]
fn an_out_of_range_probability_saturates_instead_of_existing() {
assert_eq!(Probability::percent(-5.0), Probability::ZERO);
assert_eq!(Probability::percent(f64::NAN), Probability::ZERO);
assert_eq!(Probability::percent(400.0), Probability::ALWAYS);
assert_eq!(Rtt::from_millis(-1.0), Rtt::ZERO);
}
#[tokio::test]
async fn a_perfect_link_delivers_every_datagram_in_order() {
let fixture = Fixture::new(Netem::uniform(Link::PERFECT, Seed::DEFAULT), 7_001);
for i in 0..16u8 {
fixture.send(&[i]).await;
}
for i in 0..16u8 {
assert_eq!(fixture.recv().await, vec![i]);
}
let impairments = fixture.sender.impairments();
assert_eq!(impairments.offered(), 16);
assert_eq!(impairments.dropped(), 0);
}
#[tokio::test]
async fn delivery_waits_out_the_injected_one_way_delay() {
let rtt = Rtt::from_millis(20.0);
let fixture = Fixture::new(Netem::uniform(Link::at(rtt), Seed::DEFAULT), 7_002);
let start = Instant::now();
fixture.send(b"slow").await;
assert!(start.elapsed() < rtt.one_way());
assert_eq!(fixture.recv().await, b"slow");
assert!(
start.elapsed() >= rtt.one_way(),
"delivered after {:?}, inside the {:?} one-way delay",
start.elapsed(),
rtt.one_way()
);
}
#[tokio::test]
async fn a_sub_millisecond_delay_is_not_rounded_up_to_the_timer_resolution() {
let rtt = Rtt::from_millis(0.1);
let fixture = Fixture::new(Netem::uniform(Link::at(rtt), Seed::DEFAULT), 7_003);
let start = Instant::now();
for i in 0..20u8 {
fixture.send(&[i]).await;
fixture.recv().await;
}
let mean = start.elapsed() / 20;
assert!(
mean >= rtt.one_way(),
"mean delivery {mean:?} is below the injected {:?}",
rtt.one_way()
);
assert!(
mean < Duration::from_micros(500),
"mean delivery {mean:?} suggests the 50 µs delay was rounded up to a timer tick"
);
}
#[tokio::test]
async fn jitter_swings_the_delay_around_the_configured_one() {
const DATAGRAMS: u32 = 12;
let delay = Rtt::from_millis(10.0).one_way();
let fixture = Fixture::new(
Netem::uniform(
Link::at(Rtt::from_millis(10.0)).with_jitter(delay),
Seed::DEFAULT,
),
7_009,
);
let (mut shortest, mut longest) = (Duration::MAX, Duration::ZERO);
for i in 0..DATAGRAMS {
let start = Instant::now();
fixture.send(&[i as u8]).await;
fixture.recv().await;
let observed = start.elapsed();
shortest = shortest.min(observed);
longest = longest.max(observed);
}
assert!(
shortest < delay && longest > delay,
"delays spanned {shortest:?}..{longest:?}, which does not straddle the configured {delay:?}"
);
assert!(
longest < delay * 2 + Duration::from_millis(2),
"delay {longest:?} exceeds the configured envelope of {:?}",
delay * 2
);
}
#[tokio::test]
async fn loss_drops_datagrams_at_about_the_configured_rate() {
const DATAGRAMS: u64 = 4_000;
let netem = Netem::uniform(
Link::PERFECT.with_loss(Probability::percent(10.0)),
Seed::DEFAULT,
);
let fixture = Fixture::new(netem, 7_004);
for _ in 0..DATAGRAMS {
fixture.send(b"maybe").await;
}
let impairments = fixture.sender.impairments();
assert_eq!(impairments.offered(), DATAGRAMS);
assert!(
(0.07..=0.13).contains(&impairments.loss_fraction()),
"realized loss {} is not near the configured 10 %",
impairments.loss_fraction()
);
}
#[tokio::test]
async fn a_replayed_seed_replays_the_same_losses() {
let (source, destination) = (
"127.5.0.1:7005".parse().unwrap(),
"127.5.0.2:7005".parse().unwrap(),
);
async fn dropped(seed: Seed, source: SocketAddr, destination: SocketAddr) -> u64 {
let netem = Netem::uniform(Link::PERFECT.with_loss(Probability::percent(25.0)), seed);
let fixture = Fixture::between(netem, source, destination);
for _ in 0..500 {
fixture.send(b"maybe").await;
}
fixture.sender.impairments().dropped()
}
let first = dropped(Seed::DEFAULT, source, destination).await;
assert_eq!(first, dropped(Seed::DEFAULT, source, destination).await);
assert_ne!(
first,
dropped(Seed::new(0xa5a5_a5a5), source, destination).await
);
}
#[tokio::test]
async fn a_per_destination_override_beats_the_default_link() {
let far = fresh_addr(3, 7_006);
let netem = Netem::uniform(Link::PERFECT, Seed::DEFAULT)
.with_link_to(far.ip(), Link::at(Rtt::from_millis(40.0)));
let fixture = Fixture::new(netem, 7_006);
let far_receiver = fixture.network.bind(far);
let start = Instant::now();
fixture.sender.send_to(b"far", &far).await.unwrap();
fixture.send(b"near").await;
assert_eq!(fixture.recv().await, b"near");
assert!(start.elapsed() < Duration::from_millis(20));
let mut buf = [0u8; 64];
let (n, _) = tokio::time::timeout(PATIENCE, far_receiver.recv_from(&mut buf))
.await
.expect("the far peer's datagram must still arrive")
.unwrap();
assert_eq!(&buf[..n], b"far");
assert!(start.elapsed() >= Duration::from_millis(20));
}
#[tokio::test]
async fn reordering_lands_a_datagram_behind_one_sent_after_it() {
let rtt = Rtt::from_millis(20.0);
let fixture = Fixture::new(
Netem::uniform(
Link::at(rtt).with_reorder(Probability::ALWAYS),
Seed::DEFAULT,
),
7_007,
);
let plain = NetemTransport::new(
Arc::new(fixture.network.bind(fresh_addr(3, 7_007))),
Netem::uniform(Link::at(rtt), Seed::DEFAULT),
);
fixture.send(b"first").await;
plain
.send_to(b"second", &fixture.destination)
.await
.unwrap();
assert_eq!(fixture.recv().await, b"second");
assert_eq!(fixture.recv().await, b"first");
}
#[tokio::test(flavor = "multi_thread")]
async fn a_replicated_map_converges_over_a_lossy_delayed_link() {
let network = InMemoryNetwork::new();
let (a_addr, b_addr) = (fresh_addr(1, 7_008), fresh_addr(2, 7_008));
let link = Link::at(Rtt::from_millis(2.0)).with_loss(Probability::percent(40.0));
let store = |addr: SocketAddr| {
let transport = NetemTransport::new(
Arc::new(network.bind(addr)),
Netem::uniform(link, Seed::DEFAULT),
);
let impairments = transport.impairments();
let store = ReplicatedMap::<u32, u32>::new_with_transport(
Config::default()
.with_port(addr.port())
.with_listen_addr(addr.ip())
.with_net("127.0.0.1/8".parse().unwrap())
.with_reconcile_interval(Duration::from_millis(20))
.with_insecure_no_key(),
Arc::new(transport),
);
(store, impairments)
};
let ((a, a_losses), (b, b_losses)) = (store(a_addr), store(b_addr));
let entries: Vec<(u32, u32)> = (0..200u32).map(|k| (k, k.wrapping_mul(3))).collect();
a.insert_bulk(&entries);
let target = a.fingerprint(..);
b.seed_peer(a_addr.ip());
let tasks = [tokio::spawn(a.clone().run()), tokio::spawn(b.clone().run())];
tokio::time::timeout(Duration::from_secs(60), async {
while b.fingerprint(..) != target {
tokio::time::sleep(Duration::from_millis(5)).await;
}
})
.await
.expect("anti-entropy must converge over a lossy link, however many rounds it takes");
let dropped = |losses: &Impairments| losses.dropped();
assert!(
dropped(&a_losses) + dropped(&b_losses) > 0,
"the link was configured to lose 10 % of datagrams but lost none, so this test proved \
nothing about convergence under loss"
);
for task in tasks {
task.abort();
}
}