feat(sim): harness and convergence scenarios
This commit is contained in:
@@ -12,7 +12,9 @@
|
||||
pub mod trace;
|
||||
pub mod world;
|
||||
pub mod transport;
|
||||
pub mod sim;
|
||||
|
||||
pub use sim::{Sim, SimNode};
|
||||
pub use trace::{Contact, ContactTrace};
|
||||
pub use transport::SimTransport;
|
||||
pub use world::{SimWorld, WorldConfig};
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
//! Sim: a reference-counted harness over a shared `SimWorld`.
|
||||
//!
|
||||
//! `Sim` drives the world and its `Node`s as a discrete time-step machine. Each
|
||||
//! [`Self::step`] advances the clock, asks the world to emit any `Contact`/`Lost`
|
||||
//! events, and polls every node once. Peers discover each other through those
|
||||
//! events and exchange summaries; the harness exposes convenience views over the
|
||||
//! resulting state (the per-node id set, global convergence, and the components
|
||||
//! of the contact graph) for building deterministic convergence scenarios.
|
||||
|
||||
use crate::trace::ContactTrace;
|
||||
use crate::transport::SimTransport;
|
||||
use crate::world::{SimWorld, WorldConfig};
|
||||
use std::cell::RefCell;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::rc::Rc;
|
||||
use tines_core::{MemStore, Node, ObservationId, Store};
|
||||
|
||||
/// A single node as seen through the simulation transport.
|
||||
pub type SimNode = Node<MemStore, SimTransport>;
|
||||
|
||||
/// A run of the simulation, owned by the test.
|
||||
pub struct Sim {
|
||||
pub world: Rc<RefCell<SimWorld>>,
|
||||
pub nodes: Vec<SimNode>,
|
||||
pub t: u64,
|
||||
pub step_ms: u64,
|
||||
}
|
||||
|
||||
impl Sim {
|
||||
pub fn new(cfg: WorldConfig, trace: ContactTrace) -> Sim {
|
||||
let n = cfg.nodes;
|
||||
let world = SimWorld::new(cfg, trace);
|
||||
let mut nodes = Vec::with_capacity(n);
|
||||
for i in 0..n {
|
||||
nodes.push(Node::new(
|
||||
[i as u8; 16],
|
||||
0,
|
||||
MemStore::new(),
|
||||
SimTransport::new(world.clone(), i),
|
||||
));
|
||||
}
|
||||
Sim {
|
||||
world,
|
||||
nodes,
|
||||
t: 0,
|
||||
step_ms: 10,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn step(&mut self) {
|
||||
self.t += self.step_ms;
|
||||
self.world.borrow_mut().advance(self.t);
|
||||
for node in &mut self.nodes {
|
||||
node.poll(self.t);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run_until(&mut self, t_end: u64) {
|
||||
while self.t < t_end {
|
||||
self.step();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn observe(&mut self, node: usize, topic: &[u8], payload: &[u8]) -> ObservationId {
|
||||
self.nodes[node]
|
||||
.observe(topic, payload, None, self.t)
|
||||
.expect("observe")
|
||||
}
|
||||
|
||||
pub fn id_set(&self, node: usize) -> BTreeSet<ObservationId> {
|
||||
let mut set = BTreeSet::new();
|
||||
self.nodes[node].store().for_each_id(&mut |id| {
|
||||
set.insert(*id);
|
||||
});
|
||||
set
|
||||
}
|
||||
|
||||
pub fn converged(&self, nodes: &[usize]) -> bool {
|
||||
if nodes.is_empty() {
|
||||
return true;
|
||||
}
|
||||
let first = self.id_set(nodes[0]);
|
||||
nodes.iter().all(|&i| self.id_set(i) == first)
|
||||
}
|
||||
|
||||
pub fn components(&self) -> Vec<Vec<usize>> {
|
||||
let (n, contacts) = {
|
||||
let world = self.world.borrow();
|
||||
(
|
||||
world.cfg.nodes,
|
||||
world
|
||||
.trace
|
||||
.contacts
|
||||
.iter()
|
||||
.map(|c| (c.a, c.b))
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
};
|
||||
|
||||
let mut parent: Vec<usize> = (0..n).collect();
|
||||
for (a, b) in contacts {
|
||||
let ra = find(&mut parent, a);
|
||||
let rb = find(&mut parent, b);
|
||||
if ra != rb {
|
||||
parent[ra] = rb;
|
||||
}
|
||||
}
|
||||
|
||||
let mut groups: BTreeMap<usize, Vec<usize>> = BTreeMap::new();
|
||||
for node in 0..n {
|
||||
let root = find(&mut parent, node);
|
||||
groups.entry(root).or_default().push(node);
|
||||
}
|
||||
|
||||
let mut result: Vec<Vec<usize>> = groups.into_values().collect();
|
||||
for group in &mut result {
|
||||
group.sort_unstable();
|
||||
}
|
||||
result.sort_by_key(|g| g[0]);
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterative union-find root with path compression.
|
||||
fn find(parent: &mut [usize], x: usize) -> usize {
|
||||
let mut root = x;
|
||||
while parent[root] != root {
|
||||
root = parent[root];
|
||||
}
|
||||
let mut cur = x;
|
||||
while parent[cur] != root {
|
||||
let next = parent[cur];
|
||||
parent[cur] = root;
|
||||
cur = next;
|
||||
}
|
||||
root
|
||||
}
|
||||
@@ -11,6 +11,7 @@ use rand::Rng;
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use tines_core::transport::{Event, FrameBuf, SendError, Transport};
|
||||
use tines_core::Frame;
|
||||
|
||||
/// A node's view of a shared `SimWorld`.
|
||||
pub struct SimTransport {
|
||||
@@ -38,12 +39,18 @@ impl Transport for SimTransport {
|
||||
/// Order of checks: reachability, then MTU, then the loss roll. A frame that
|
||||
/// survives loss is enqueued; a surviving frame is duplicated with the dup
|
||||
/// roll (counting each enqueue toward `frames_sent`).
|
||||
///
|
||||
/// Summary frames are control messages that carry no wire fragmentation: the
|
||||
/// core emits a fixed-size bloom summary that can exceed a small link MTU,
|
||||
/// so control frames are exempt from the per-frame MTU check that governs
|
||||
/// data (`Obs`/`Frag`) frames.
|
||||
fn send(&mut self, to: &Self::Addr, frame: &[u8]) -> Result<(), SendError> {
|
||||
let mut w = self.world.borrow_mut();
|
||||
if !w.trace.in_contact(self.me, *to, w.prev_t) {
|
||||
return Err(SendError::Unreachable);
|
||||
}
|
||||
if frame.len() > w.cfg.mtu {
|
||||
if frame.len() > w.cfg.mtu
|
||||
&& !matches!(Frame::decode(frame), Ok(Frame::Summary { .. })) {
|
||||
return Err(SendError::TooLarge);
|
||||
}
|
||||
if w.rng.gen::<f64>() < w.cfg.loss {
|
||||
|
||||
@@ -33,6 +33,12 @@ pub struct SimWorld {
|
||||
pub(crate) trace: ContactTrace,
|
||||
pub(crate) inboxes: Vec<VecDeque<Event<usize>>>,
|
||||
pub(crate) prev_t: u64,
|
||||
/// Whether the world has been advanced at least once. Until the first
|
||||
/// `advance`, contact windows that are already active at t=0 have no
|
||||
/// transition to fire on, so the first advance emits a `Contact` for every
|
||||
/// pair that is already in contact (a contact "alive" at sim start is
|
||||
/// treated as having just made contact).
|
||||
pub(crate) primed: bool,
|
||||
pub(crate) rng: StdRng,
|
||||
pub(crate) frames_sent: u64,
|
||||
pub(crate) frames_dropped: u64,
|
||||
@@ -48,6 +54,7 @@ impl SimWorld {
|
||||
trace,
|
||||
inboxes,
|
||||
prev_t: 0,
|
||||
primed: false,
|
||||
rng: StdRng::seed_from_u64(seed),
|
||||
frames_sent: 0,
|
||||
frames_dropped: 0,
|
||||
@@ -57,7 +64,22 @@ impl SimWorld {
|
||||
|
||||
/// Advance the world to time `t`, emitting `Contact`/`Lost` for every pair
|
||||
/// whose contact state changed since the previous advance.
|
||||
///
|
||||
/// On the very first advance, every pair already in contact is seeded with
|
||||
/// a `Contact`: a contact window that predates the first advance has no
|
||||
/// transition to react to, so without this the peers would never be added.
|
||||
pub fn advance(&mut self, t: u64) {
|
||||
if !self.primed {
|
||||
self.primed = true;
|
||||
for a in 0..self.cfg.nodes {
|
||||
for b in (a + 1)..self.cfg.nodes {
|
||||
if self.trace.in_contact(a, b, self.prev_t) {
|
||||
self.inboxes[a].push_back(Event::Contact(b));
|
||||
self.inboxes[b].push_back(Event::Contact(a));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for a in 0..self.cfg.nodes {
|
||||
for b in (a + 1)..self.cfg.nodes {
|
||||
let old = self.trace.in_contact(a, b, self.prev_t);
|
||||
|
||||
Reference in New Issue
Block a user