use std::collections::HashSet;
use std::net::SocketAddr;
use std::time::{Duration, Instant};
use rand::seq::IndexedRandom;
use rand::{RngExt, SeedableRng};
use rand_chacha::ChaCha20Rng;
use tokio::time;
use crate::config::NodeConfig;
use crate::node::Node;
pub fn sim_config() -> NodeConfig {
NodeConfig {
view_size: 16,
sample_size: 6,
exchange_interval: Duration::from_millis(200),
cover: crate::config::CoverStrategy::Constant {
interval: Duration::from_millis(50),
},
frame_size: 256,
max_connections: 64,
..Default::default()
}
}
pub struct Simulation {
rng: ChaCha20Rng,
nodes: Vec<Node>,
addrs: Vec<SocketAddr>,
alive_set: HashSet<usize>,
started_at: Instant,
partition: Option<(Vec<usize>, Vec<usize>)>,
}
#[derive(Clone, Debug, Default)]
pub struct Metrics {
pub elapsed_secs: f64,
pub alive: usize,
pub total: usize,
pub avg_view_size: f64,
pub avg_trusted: f64,
pub avg_recent: f64,
pub avg_random: f64,
pub avg_bootstrap: f64,
pub coverage: f64,
}
impl Simulation {
pub async fn new(n: usize, seed: u64, config: NodeConfig) -> Self {
let rng = ChaCha20Rng::seed_from_u64(seed);
let mut nodes = Vec::with_capacity(n);
let mut addrs = Vec::with_capacity(n);
let mut alive_set = HashSet::with_capacity(n);
for i in 0..n {
let cfg = NodeConfig {
name: Some(format!("sim-{i:03}")),
listener_addr: Some("127.0.0.1:0".parse().unwrap()),
..config.clone()
};
let node_seed = seed
.wrapping_add(i as u64)
.wrapping_mul(0x9E37_79B9_7F4A_7C15);
let node = Node::with_seed(cfg, node_seed);
let addr = node.spawn().await.expect("spawn");
let addr = addr.expect("listener bound");
nodes.push(node);
addrs.push(addr);
alive_set.insert(i);
}
Self {
rng,
nodes,
addrs,
alive_set,
started_at: Instant::now(),
partition: None,
}
}
pub fn len(&self) -> usize {
self.nodes.len()
}
pub fn is_empty(&self) -> bool {
self.nodes.is_empty()
}
pub fn node(&self, i: usize) -> &Node {
&self.nodes[i]
}
pub fn nodes(&self) -> &[Node] {
&self.nodes
}
pub fn addr(&self, i: usize) -> SocketAddr {
self.addrs[i]
}
pub fn alive(&self) -> Vec<usize> {
let mut v: Vec<usize> = self.alive_set.iter().copied().collect();
v.sort();
v
}
pub fn random_alive(&mut self) -> Option<usize> {
let alive: Vec<usize> = self.alive_set.iter().copied().collect();
alive.choose(&mut self.rng).copied()
}
pub fn metrics(&self) -> Metrics {
let mut m = Metrics {
elapsed_secs: self.started_at.elapsed().as_secs_f64(),
..Metrics::default()
};
let alive: Vec<usize> = self.alive();
m.alive = alive.len();
m.total = self.nodes.len();
if alive.is_empty() {
return m;
}
let mut total_view = 0.0;
let mut total_trusted = 0.0;
let mut total_recent = 0.0;
let mut total_random = 0.0;
let mut total_bootstrap = 0.0;
let mut total_coverage_n = 0.0;
let total = alive.len();
let alive_set: HashSet<SocketAddr> = alive.iter().map(|&i| self.addrs[i]).collect();
for &i in &alive {
let snap = self.nodes[i].view();
total_trusted += snap.trusted.len() as f64;
total_recent += snap.recent.len() as f64;
total_random += snap.random.len() as f64;
total_bootstrap += snap.bootstrap.len() as f64;
total_view += (snap.trusted.len() + snap.recent.len() + snap.random.len()) as f64;
let known: HashSet<SocketAddr> = snap
.trusted
.iter()
.chain(snap.recent.iter())
.chain(snap.random.iter())
.chain(snap.bootstrap.iter())
.map(|p| p.addr)
.collect();
let known_alive: usize = known.intersection(&alive_set).count();
total_coverage_n += known_alive as f64 / (total as f64 - 1.0).max(1.0);
}
let denom = alive.len() as f64;
m.avg_view_size = total_view / denom;
m.avg_trusted = total_trusted / denom;
m.avg_recent = total_recent / denom;
m.avg_random = total_random / denom;
m.avg_bootstrap = total_bootstrap / denom;
m.coverage = total_coverage_n / denom;
m
}
pub async fn connect_mesh(&self) {
for i in 0..self.nodes.len() {
for j in 0..self.nodes.len() {
if i == j {
continue;
}
if self.crosses_partition(i, j) {
continue;
}
let _ = self.nodes[i].connect(self.addrs[j]).await;
}
}
}
pub async fn connect_ring(&self) {
let n = self.nodes.len();
for i in 0..n {
for &j in &[(i + 1) % n, (i + n - 1) % n] {
if self.crosses_partition(i, j) {
continue;
}
let _ = self.nodes[i].connect(self.addrs[j]).await;
}
}
}
pub async fn kill(&mut self, i: usize) {
if !self.alive_set.contains(&i) {
return;
}
let dead_addr = self.addrs[i];
for peer in self.nodes[i].connected_peers() {
let _ = self.nodes[i].disconnect(peer).await;
}
self.nodes[i].shutdown().await;
self.alive_set.remove(&i);
for &j in self.alive_set.iter() {
self.nodes[j].drop_peer(&dead_addr);
}
}
pub async fn partition(&mut self, group_a: Vec<usize>, group_b: Vec<usize>) {
self.partition = Some((group_a.clone(), group_b.clone()));
for &i in &group_a {
for &j in &group_b {
if self.nodes[i].connected_peers().contains(&self.addrs[j]) {
let _ = self.nodes[i].disconnect(self.addrs[j]).await;
}
}
}
}
pub async fn heal_partition(&mut self) {
let Some((a, b)) = self.partition.take() else {
return;
};
for &i in &a {
for &j in &b {
if !self.nodes[i].connected_peers().contains(&self.addrs[j]) {
let _ = self.nodes[i].connect(self.addrs[j]).await;
}
}
}
}
pub async fn inject_churn(&mut self, p: f64) {
let to_kill: Vec<usize> = {
let alive: Vec<usize> = self.alive_set.iter().copied().collect();
alive
.into_iter()
.filter(|_| self.rng.random::<f64>() < p)
.collect()
};
for i in to_kill {
self.kill(i).await;
}
}
pub async fn step(&self, dur: Duration) {
time::sleep(dur).await;
}
pub async fn shutdown(self) {
let alive = self.alive();
for i in alive {
self.nodes[i].shutdown().await;
}
}
fn crosses_partition(&self, i: usize, j: usize) -> bool {
if let Some((a, b)) = &self.partition {
(a.contains(&i) && b.contains(&j)) || (a.contains(&j) && b.contains(&i))
} else {
false
}
}
}