feat(sim): world, contact trace, SimTransport

This commit is contained in:
2026-10-17 16:06:51 -07:00
parent 1740acd625
commit 5dca574c50
6 changed files with 849 additions and 2 deletions
Generated
+461 -2
View File
@@ -2,6 +2,37 @@
# It is not intended for manual editing.
version = 4
[[package]]
name = "autocfg"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
[[package]]
name = "bit-set"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56d87354e4229f54a44f7bf2435906a4656dba36026ab6eaca629a2c436a691c"
dependencies = [
"bit-vec",
]
[[package]]
name = "bit-vec"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5727b15fa97d4f4fee0a3b7c3d550ed0269f54329207b86388de918604e31269"
dependencies = [
"borsh",
"serde",
]
[[package]]
name = "bitflags"
version = "2.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
[[package]]
name = "blake2"
version = "0.10.6"
@@ -20,12 +51,65 @@ dependencies = [
"generic-array",
]
[[package]]
name = "borsh"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "553c5d846a6ba5150c65e3b1b8ec073bcf1abc20f9b7220de384a4443ea4e20a"
dependencies = [
"borsh-derive",
"bytes",
"cfg_aliases",
]
[[package]]
name = "borsh-derive"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12cdfe656708a01f89b451a7d36466e6fe6c414de0aa18fc54f864f6f9ca9f56"
dependencies = [
"once_cell",
"proc-macro-crate",
"proc-macro2",
"quote",
"syn 3.0.9",
]
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
version = "1.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
[[package]]
name = "cfg-if"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
[[package]]
name = "cfg_aliases"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
[[package]]
name = "chacha20"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06"
dependencies = [
"cfg-if",
"cpufeatures",
"rand_core 0.10.1",
]
[[package]]
name = "cobs"
version = "0.3.0"
@@ -35,6 +119,21 @@ dependencies = [
"thiserror",
]
[[package]]
name = "core_detect"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48"
[[package]]
name = "cpufeatures"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566"
dependencies = [
"libc",
]
[[package]]
name = "crypto-common"
version = "0.1.7"
@@ -56,6 +155,34 @@ dependencies = [
"subtle",
]
[[package]]
name = "equivalent"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826"
[[package]]
name = "errno"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys",
]
[[package]]
name = "fastrand"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "generic-array"
version = "0.14.7"
@@ -66,6 +193,29 @@ dependencies = [
"version_check",
]
[[package]]
name = "getrandom"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "getrandom"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"rand_core 0.10.1",
]
[[package]]
name = "hash32"
version = "0.3.1"
@@ -75,6 +225,12 @@ dependencies = [
"byteorder",
]
[[package]]
name = "hashbrown"
version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
[[package]]
name = "heapless"
version = "0.8.0"
@@ -86,6 +242,49 @@ dependencies = [
"stable_deref_trait",
]
[[package]]
name = "indexmap"
version = "2.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "libc"
version = "0.2.191"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a"
[[package]]
name = "linux-raw-sys"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]]
name = "memchr"
version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "postcard"
version = "1.1.3"
@@ -96,6 +295,24 @@ dependencies = [
"serde",
]
[[package]]
name = "ppv-lite86"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
dependencies = [
"zerocopy",
]
[[package]]
name = "proc-macro-crate"
version = "3.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
dependencies = [
"toml_edit",
]
[[package]]
name = "proc-macro2"
version = "1.0.107"
@@ -105,6 +322,32 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "proptest"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8530004ccb15eae51c7e40009fbe317f341f804db54dc033eec1c50be28cfa0"
dependencies = [
"bit-set",
"bit-vec",
"bitflags",
"chacha20",
"core_detect",
"num-traits",
"rand 0.10.3",
"rand_xorshift",
"regex-syntax",
"rusty-fork",
"tempfile",
"unarray",
]
[[package]]
name = "quick-error"
version = "1.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
[[package]]
name = "quote"
version = "1.0.47"
@@ -114,6 +357,98 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
name = "rand"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c"
dependencies = [
"libc",
"rand_chacha",
"rand_core 0.6.4",
]
[[package]]
name = "rand"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af"
dependencies = [
"getrandom 0.4.3",
"rand_core 0.10.1",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core 0.6.4",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom 0.2.17",
]
[[package]]
name = "rand_core"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
[[package]]
name = "rand_xorshift"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60aa6af80be32871323012e02e6e65f8a7cc7890931ae421d217ad8fe0df2ccf"
dependencies = [
"rand_core 0.10.1",
]
[[package]]
name = "regex-syntax"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
[[package]]
name = "rustix"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d"
dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys",
"windows-sys",
]
[[package]]
name = "rusty-fork"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2"
dependencies = [
"fnv",
"quick-error",
"tempfile",
"wait-timeout",
]
[[package]]
name = "serde"
version = "1.0.229"
@@ -141,7 +476,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 3.0.9",
]
[[package]]
@@ -156,6 +491,17 @@ version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
version = "2.0.119"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn"
version = "3.0.9"
@@ -167,6 +513,19 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "tempfile"
version = "3.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [
"fastrand",
"getrandom 0.4.3",
"once_cell",
"rustix",
"windows-sys",
]
[[package]]
name = "thiserror"
version = "2.0.21"
@@ -184,7 +543,7 @@ checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 3.0.9",
]
[[package]]
@@ -200,6 +559,41 @@ dependencies = [
[[package]]
name = "tines-sim"
version = "0.1.0"
dependencies = [
"proptest",
"rand 0.8.8",
"tines-core",
]
[[package]]
name = "toml_datetime"
version = "1.1.2+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b86d767906c6c42421dcba507eb9d203e779497710a47782a224bb871653053"
dependencies = [
"serde_core",
]
[[package]]
name = "toml_edit"
version = "0.25.17+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3641d5bbb5349a79e1020a242d251efbc546ad8048d133958323ce9c40a9c9c"
dependencies = [
"indexmap",
"toml_datetime",
"toml_parser",
"winnow",
]
[[package]]
name = "toml_parser"
version = "1.1.5+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baa693a8032d7e1cada7d0041e96126df243179ff061456783ac7f12bda4744c"
dependencies = [
"winnow",
]
[[package]]
name = "typenum"
@@ -207,6 +601,12 @@ version = "1.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
[[package]]
name = "unarray"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94"
[[package]]
name = "unicode-ident"
version = "1.0.27"
@@ -218,3 +618,62 @@ name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "wait-timeout"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11"
dependencies = [
"libc",
]
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "winnow"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81"
dependencies = [
"memchr",
]
[[package]]
name = "zerocopy"
version = "0.8.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5fe1f8f1b06191a00962174c61aa5005e0bb391a6d80d07e24d115c01a92ed8"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "863ad3ac83293fb4d740aedbfdc9240dd8d1a50c1099acd76ce80ce7c7230c7f"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
+7
View File
@@ -2,3 +2,10 @@
name = "tines-sim"
version = "0.1.0"
edition = "2021"
[dependencies]
tines-core = { path = "../tines-core" }
rand = "0.8"
[dev-dependencies]
proptest = "1"
+18
View File
@@ -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};
+68
View File
@@ -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);
}
}
+67
View File
@@ -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
}
}
+228
View File
@@ -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());
}
}