summaryrefslogtreecommitdiffstats
path: root/src/simtest/soak.rs
blob: 6906238f689807aadaa83229f9e47c1dd5360db8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
//! Many nodes under realistic conditions for a while. Ignored by default (a few
//! minutes in a debug build): `cargo test soak -- --ignored --nocapture`.

use std::time::Duration;

use serde_json::json;

use super::{Sim, write};
use crate::state::AppState;
use crate::swarm::bootstrap::Start;
use crate::swarm::members;
use crate::swarm::stats::Propagation;
use crate::transport::sim::LinkParams;

const NODES: usize = 25;
/// One per second, round robin over the nodes.
const WRITES: usize = 60;
/// 99% of changes reach a node within this, from the writer's commit. Measured: p99
/// within the 2.5 s bucket, mean about 0.7 s (October 2026); the bound is the next
/// bucket up.
const P99_BOUND_MS: u64 = 5_000;

/// 25 nodes (a partial mesh: 8 neighbours each) over links with 2% loss, 1%
/// duplication and 50–500 ms delay; one write per second for a minute. All converge,
/// and 99% of changes reach each node within `P99_BOUND_MS`.
#[tokio::test(flavor = "multi_thread", worker_threads = 8)]
#[ignore]
async fn soak_25_nodes() {
    let sim = Sim::new(25);
    sim.net.set_default_link(LinkParams {
        loss: 0.02,
        duplicate: 0.01,
        delay: (Duration::from_millis(50), Duration::from_millis(500)),
        ..LinkParams::default()
    });
    let ids: Vec<String> = (0..NODES).map(|i| format!("n{i:02}")).collect();
    let mut nodes = vec![sim.start(&ids[0], &[], Start::Genesis)];
    for id in &ids[1..] {
        nodes.push(sim.start(id, &[ids[0].as_str()], Start::Join));
    }
    let all: Vec<&AppState> = nodes.iter().collect();
    let all = all.as_slice();
    sim.wait_ready(all, Duration::from_secs(120)).await;
    sim.wait_until(
        "every node knows every member",
        Duration::from_secs(60),
        move || async move {
            for node in all {
                if members::members(&node.doc.read().await.doc).len() < NODES {
                    return false;
                }
            }
            true
        },
    )
    .await;

    for i in 0..WRITES {
        write(&nodes[i % NODES], &format!("soak/{i}"), json!(i)).await;
        tokio::time::sleep(Duration::from_secs(1)).await;
    }
    sim.wait_converged(all, Duration::from_secs(60)).await;

    let mut total = Propagation::default();
    for node in all {
        let propagation = node.stats.snapshot().propagation;
        for (sum, count) in total.buckets.iter_mut().zip(propagation.buckets) {
            *sum += count;
        }
        total.count += propagation.count;
        total.sum_ms += propagation.sum_ms;
    }
    let p99 = percentile_bound(&total, 0.99);
    println!(
        "propagation: {} changes, mean {} ms, p99 within {}, buckets {:?}; net {:?}",
        total.count,
        total.sum_ms / total.count.max(1),
        p99.map_or("more than an hour".to_owned(), |ms| format!("{ms} ms")),
        total.buckets().collect::<Vec<_>>(),
        sim.net.counts(),
    );
    assert!(total.count > 0);
    assert!(
        p99.is_some_and(|ms| ms <= P99_BOUND_MS),
        "p99 propagation above {P99_BOUND_MS} ms"
    );
}

/// The upper bound of the bucket in which the `quantile` of changes is reached; None if
/// only in the last (unbounded) one.
fn percentile_bound(propagation: &Propagation, quantile: f64) -> Option<u64> {
    let wanted = (propagation.count as f64 * quantile).ceil() as u64;
    let mut seen = 0;
    for (bound, count) in propagation.buckets() {
        seen += count;
        if seen >= wanted {
            return bound;
        }
    }
    None
}