feat(sim): world, contact trace, SimTransport
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
//! tines-sim: a std simulator for the tines shared-memory library.
|
||||
//!
|
||||
//! `SimWorld` is a shared, reference-counted simulation of nodes connected by a
|
||||
//! `ContactTrace`. `SimTransport` implements the `tines_core::Transport` trait
|
||||
//! on top of that world: outbound frames are enqueued into per-node inboxes,
|
||||
//! and `SimWorld::advance` turns contact-state changes into `Contact`/`Lost`
|
||||
//! events. Loss and duplication are driven by a deterministic, seeded RNG so
|
||||
//! that convergence behaviour is reproducible.
|
||||
|
||||
#![deny(warnings)]
|
||||
|
||||
pub mod trace;
|
||||
pub mod world;
|
||||
pub mod transport;
|
||||
|
||||
pub use trace::{Contact, ContactTrace};
|
||||
pub use transport::SimTransport;
|
||||
pub use world::{SimWorld, WorldConfig};
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
//! Contact traces: a timeline of which nodes are within range of each other.
|
||||
|
||||
/// A single contact window between two nodes.
|
||||
///
|
||||
/// Nodes `a` and `b` are in contact during the half-open interval
|
||||
/// `[start_ms, end_ms)`. Contacts are symmetric: a contact recorded as
|
||||
/// `(a, b)` is equivalent to `(b, a)`.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Contact {
|
||||
pub start_ms: u64,
|
||||
pub end_ms: u64,
|
||||
pub a: usize,
|
||||
pub b: usize,
|
||||
}
|
||||
|
||||
/// An ordered list of contact windows, as produced by the contact simulator.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct ContactTrace {
|
||||
pub contacts: Vec<Contact>,
|
||||
}
|
||||
|
||||
impl ContactTrace {
|
||||
/// Whether `a` and `b` are in contact at time `t`, for any contact
|
||||
/// involving the pair in either order.
|
||||
///
|
||||
/// A contact counts during its half-open window: `start_ms <= t < end_ms`.
|
||||
pub fn in_contact(&self, a: usize, b: usize, t: u64) -> bool {
|
||||
self.contacts
|
||||
.iter()
|
||||
.any(|c| is_contact_pair(c, a, b) && in_window(c, t))
|
||||
}
|
||||
|
||||
/// The last time any contact ends, or `0` if there are no contacts.
|
||||
pub fn end(&self) -> u64 {
|
||||
self.contacts.iter().map(|c| c.end_ms).max().unwrap_or(0)
|
||||
}
|
||||
}
|
||||
|
||||
/// True when `c` links `a` and `b` in either order.
|
||||
fn is_contact_pair(c: &Contact, a: usize, b: usize) -> bool {
|
||||
(c.a == a && c.b == b) || (c.a == b && c.b == a)
|
||||
}
|
||||
|
||||
/// True when `t` falls inside the half-open window `[start, end)`.
|
||||
fn in_window(c: &Contact, t: u64) -> bool {
|
||||
c.start_ms <= t && t < c.end_ms
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{Contact, ContactTrace};
|
||||
|
||||
#[test]
|
||||
fn in_contact_matches_both_orders() {
|
||||
let mut trace = ContactTrace::default();
|
||||
trace.contacts.push(Contact { start_ms: 100, end_ms: 200, a: 3, b: 7 });
|
||||
assert!(!trace.in_contact(3, 7, 50)); // before the window
|
||||
assert!(trace.in_contact(3, 7, 100)); // at the start edge
|
||||
assert!(trace.in_contact(7, 3, 150)); // mid-window, either order
|
||||
assert!(!trace.in_contact(3, 7, 200)); // at the end edge (half-open)
|
||||
assert!(!trace.in_contact(3, 7, 300)); // after the window
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn end_is_zero_when_empty() {
|
||||
assert_eq!(ContactTrace::default().end(), 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
//! SimTransport: a `Transport` implementation over a shared `SimWorld`.
|
||||
//!
|
||||
//! `SimTransport` is a datagram pipe bound to one node (`me`). Outbound frames
|
||||
//! are checked against the live contact graph and the world's MTU, then
|
||||
//! subjected to a loss roll and — on success — a duplication roll. All of this
|
||||
//! mutates the shared `SimWorld`'s per-node mailboxes and frame counters, so
|
||||
//! every participant shares one view of the network.
|
||||
|
||||
use crate::world::SimWorld;
|
||||
use rand::Rng;
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use tines_core::transport::{Event, FrameBuf, SendError, Transport};
|
||||
|
||||
/// A node's view of a shared `SimWorld`.
|
||||
pub struct SimTransport {
|
||||
world: Rc<RefCell<SimWorld>>,
|
||||
me: usize,
|
||||
}
|
||||
|
||||
impl SimTransport {
|
||||
/// Bind transport `me` to the shared `world`.
|
||||
pub fn new(world: Rc<RefCell<SimWorld>>, me: usize) -> Self {
|
||||
SimTransport { world, me }
|
||||
}
|
||||
}
|
||||
|
||||
impl Transport for SimTransport {
|
||||
type Addr = usize;
|
||||
|
||||
/// Pop the next queued event for this node, if any.
|
||||
fn poll(&mut self) -> Option<Event<Self::Addr>> {
|
||||
self.world.borrow_mut().inboxes[self.me].pop_front()
|
||||
}
|
||||
|
||||
/// Attempt to deliver `frame` to `to`.
|
||||
///
|
||||
/// 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`).
|
||||
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 {
|
||||
return Err(SendError::TooLarge);
|
||||
}
|
||||
if w.rng.gen::<f64>() < w.cfg.loss {
|
||||
w.frames_dropped += 1;
|
||||
return Ok(());
|
||||
}
|
||||
let buf = FrameBuf::from_slice(frame).map_err(|_| SendError::TooLarge)?;
|
||||
w.inboxes[*to].push_back(Event::Frame(self.me, buf.clone()));
|
||||
w.frames_sent += 1;
|
||||
if w.rng.gen::<f64>() < w.cfg.dup {
|
||||
w.inboxes[*to].push_back(Event::Frame(self.me, buf.clone()));
|
||||
w.frames_sent += 1;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Maximum frame size in bytes this transport can send.
|
||||
fn mtu(&self) -> usize {
|
||||
self.world.borrow().cfg.mtu
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
//! World configuration and the shared `SimWorld`.
|
||||
//!
|
||||
//! `SimWorld` is the heart of the simulator: it owns the contact trace, a
|
||||
//! per-node inbound mailbox, the last-advanced time, and a seeded RNG that
|
||||
//! governs loss and duplication. It is always held behind `Rc<RefCell<..>>` so
|
||||
//! that many `SimTransport`s can share a single world.
|
||||
|
||||
use crate::trace::ContactTrace;
|
||||
use rand::{rngs::StdRng, SeedableRng};
|
||||
use std::cell::RefCell;
|
||||
use std::collections::VecDeque;
|
||||
use std::rc::Rc;
|
||||
use tines_core::transport::Event;
|
||||
|
||||
/// World configuration for a `SimWorld`.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct WorldConfig {
|
||||
pub nodes: usize,
|
||||
pub mtu: usize,
|
||||
pub loss: f64,
|
||||
pub dup: f64,
|
||||
pub seed: u64,
|
||||
}
|
||||
|
||||
/// A shared simulation of `nodes` connected by a `ContactTrace`.
|
||||
///
|
||||
/// Every node owns an inbound mailbox of `Event`s. `advance` walks every
|
||||
/// unordered pair, emitting `Contact`/`Lost` where the contact graph changed
|
||||
/// since the last advance. `SimTransport` uses the same mailboxes and the same
|
||||
/// RNG (held here) to deliver and drop frames.
|
||||
pub struct SimWorld {
|
||||
pub(crate) cfg: WorldConfig,
|
||||
pub(crate) trace: ContactTrace,
|
||||
pub(crate) inboxes: Vec<VecDeque<Event<usize>>>,
|
||||
pub(crate) prev_t: u64,
|
||||
pub(crate) rng: StdRng,
|
||||
pub(crate) frames_sent: u64,
|
||||
pub(crate) frames_dropped: u64,
|
||||
}
|
||||
|
||||
impl SimWorld {
|
||||
/// Build a world with empty mailboxes, `prev_t = 0`, and a seeded RNG.
|
||||
pub fn new(cfg: WorldConfig, trace: ContactTrace) -> Rc<RefCell<SimWorld>> {
|
||||
let seed = cfg.seed;
|
||||
let inboxes = vec![VecDeque::new(); cfg.nodes];
|
||||
let world = SimWorld {
|
||||
cfg,
|
||||
trace,
|
||||
inboxes,
|
||||
prev_t: 0,
|
||||
rng: StdRng::seed_from_u64(seed),
|
||||
frames_sent: 0,
|
||||
frames_dropped: 0,
|
||||
};
|
||||
Rc::new(RefCell::new(world))
|
||||
}
|
||||
|
||||
/// Advance the world to time `t`, emitting `Contact`/`Lost` for every pair
|
||||
/// whose contact state changed since the previous advance.
|
||||
pub fn advance(&mut self, t: u64) {
|
||||
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);
|
||||
let new = self.trace.in_contact(a, b, t);
|
||||
if !old && new {
|
||||
self.inboxes[a].push_back(Event::Contact(b));
|
||||
self.inboxes[b].push_back(Event::Contact(a));
|
||||
} else if old && !new {
|
||||
self.inboxes[a].push_back(Event::Lost(b));
|
||||
self.inboxes[b].push_back(Event::Lost(a));
|
||||
}
|
||||
}
|
||||
}
|
||||
self.prev_t = t;
|
||||
}
|
||||
|
||||
/// Total frames successfully enqueued (duplication counts each enqueue).
|
||||
pub fn frames_sent(&self) -> u64 {
|
||||
self.frames_sent
|
||||
}
|
||||
|
||||
/// Total frames dropped by the loss roll-out.
|
||||
pub fn frames_dropped(&self) -> u64 {
|
||||
self.frames_dropped
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{SimWorld, WorldConfig};
|
||||
use crate::trace::{Contact, ContactTrace};
|
||||
use crate::transport::SimTransport;
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use tines_core::transport::{Event, SendError, Transport};
|
||||
|
||||
/// A 2-node world where nodes 0 and 1 are in contact during
|
||||
/// `[start, end)`, with the given loss/dup/mtu/seed.
|
||||
fn two_node_world(
|
||||
mtu: usize,
|
||||
loss: f64,
|
||||
dup: f64,
|
||||
seed: u64,
|
||||
start: u64,
|
||||
end: u64,
|
||||
) -> Rc<RefCell<SimWorld>> {
|
||||
let cfg = WorldConfig {
|
||||
nodes: 2,
|
||||
mtu,
|
||||
loss,
|
||||
dup,
|
||||
seed,
|
||||
};
|
||||
let mut trace = ContactTrace::default();
|
||||
trace
|
||||
.contacts
|
||||
.push(Contact { start_ms: start, end_ms: end, a: 0, b: 1 });
|
||||
SimWorld::new(cfg, trace)
|
||||
}
|
||||
|
||||
/// A snapshot of everything currently queued in `node`'s inbox.
|
||||
fn inbox(world: &Rc<RefCell<SimWorld>>, node: usize) -> Vec<Event<usize>> {
|
||||
world.borrow().inboxes[node].iter().cloned().collect()
|
||||
}
|
||||
|
||||
/// The number of `Frame` events queued in `node`'s inbox.
|
||||
fn frame_count(world: &Rc<RefCell<SimWorld>>, node: usize) -> usize {
|
||||
world
|
||||
.borrow()
|
||||
.inboxes[node]
|
||||
.iter()
|
||||
.filter(|e| matches!(e, Event::Frame(_, _)))
|
||||
.count()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn advance_emits_contact_and_lost_at_window_edges() {
|
||||
let world = two_node_world(1500, 0.0, 0.0, 42, 100, 200);
|
||||
|
||||
// Before the window opens, neither mailbox has anything.
|
||||
world.borrow_mut().advance(50);
|
||||
assert_eq!(inbox(&world, 0), Vec::<Event<usize>>::new());
|
||||
assert_eq!(inbox(&world, 1), Vec::<Event<usize>>::new());
|
||||
|
||||
// At the start edge both peers learn of the contact.
|
||||
world.borrow_mut().advance(100);
|
||||
assert_eq!(inbox(&world, 0), vec![Event::Contact(1)]);
|
||||
assert_eq!(inbox(&world, 1), vec![Event::Contact(0)]);
|
||||
|
||||
// At the end edge both peers learn of the loss.
|
||||
world.borrow_mut().advance(200);
|
||||
assert_eq!(inbox(&world, 0), vec![Event::Contact(1), Event::Lost(1)]);
|
||||
assert_eq!(inbox(&world, 1), vec![Event::Contact(0), Event::Lost(0)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn send_outside_contact_is_unreachable() {
|
||||
let cfg = WorldConfig {
|
||||
nodes: 2,
|
||||
mtu: 1500,
|
||||
loss: 0.0,
|
||||
dup: 0.0,
|
||||
seed: 42,
|
||||
};
|
||||
let world = SimWorld::new(cfg, ContactTrace::default());
|
||||
let mut t0 = SimTransport::new(world.clone(), 0);
|
||||
assert_eq!(t0.send(&1, &[1, 2, 3]), Err(SendError::Unreachable));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn send_over_mtu_is_too_large() {
|
||||
let world = two_node_world(8, 0.0, 0.0, 42, 0, 1000);
|
||||
let mut t0 = SimTransport::new(world.clone(), 0);
|
||||
assert_eq!(t0.send(&1, &[0u8; 9]), Err(SendError::TooLarge));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn loss_1_drops_everything() {
|
||||
let world = two_node_world(1500, 1.0, 0.0, 42, 0, 1000);
|
||||
let mut t0 = SimTransport::new(world.clone(), 0);
|
||||
for _ in 0..100 {
|
||||
t0.send(&1, &[1, 2, 3]).unwrap();
|
||||
}
|
||||
assert_eq!(frame_count(&world, 1), 0);
|
||||
assert_eq!(world.borrow().frames_dropped(), 100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dup_1_doubles_everything() {
|
||||
let world = two_node_world(1500, 0.0, 1.0, 42, 0, 1000);
|
||||
let mut t0 = SimTransport::new(world.clone(), 0);
|
||||
for _ in 0..10 {
|
||||
t0.send(&1, &[1, 2, 3]).unwrap();
|
||||
}
|
||||
assert_eq!(frame_count(&world, 1), 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn same_seed_same_drops() {
|
||||
let cfg = WorldConfig {
|
||||
nodes: 2,
|
||||
mtu: 1500,
|
||||
loss: 0.5,
|
||||
dup: 0.0,
|
||||
seed: 1234,
|
||||
};
|
||||
let make_trace = || {
|
||||
let mut trace = ContactTrace::default();
|
||||
trace
|
||||
.contacts
|
||||
.push(Contact { start_ms: 0, end_ms: 1000, a: 0, b: 1 });
|
||||
trace
|
||||
};
|
||||
let w1 = SimWorld::new(cfg.clone(), make_trace());
|
||||
let w2 = SimWorld::new(cfg.clone(), make_trace());
|
||||
let mut t1 = SimTransport::new(w1.clone(), 0);
|
||||
let mut t2 = SimTransport::new(w2.clone(), 0);
|
||||
for _ in 0..50 {
|
||||
t1.send(&1, &[1]).unwrap();
|
||||
t2.send(&1, &[1]).unwrap();
|
||||
}
|
||||
assert_eq!(w1.borrow().frames_dropped(), w2.borrow().frames_dropped());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user