diff --git a/Cargo.lock b/Cargo.lock index 8fa4bdf..29802c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", +] diff --git a/tines-sim/Cargo.toml b/tines-sim/Cargo.toml index 7bb0cc5..63c50ed 100644 --- a/tines-sim/Cargo.toml +++ b/tines-sim/Cargo.toml @@ -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" diff --git a/tines-sim/src/lib.rs b/tines-sim/src/lib.rs index e69de29..2b7ba6a 100644 --- a/tines-sim/src/lib.rs +++ b/tines-sim/src/lib.rs @@ -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}; diff --git a/tines-sim/src/trace.rs b/tines-sim/src/trace.rs new file mode 100644 index 0000000..dd6bf6a --- /dev/null +++ b/tines-sim/src/trace.rs @@ -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, +} + +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); + } +} diff --git a/tines-sim/src/transport.rs b/tines-sim/src/transport.rs new file mode 100644 index 0000000..c5f76e9 --- /dev/null +++ b/tines-sim/src/transport.rs @@ -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>, + me: usize, +} + +impl SimTransport { + /// Bind transport `me` to the shared `world`. + pub fn new(world: Rc>, 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> { + 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::() < 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::() < 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 + } +} diff --git a/tines-sim/src/world.rs b/tines-sim/src/world.rs new file mode 100644 index 0000000..6851e36 --- /dev/null +++ b/tines-sim/src/world.rs @@ -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>` 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>>, + 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> { + 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> { + 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>, node: usize) -> Vec> { + world.borrow().inboxes[node].iter().cloned().collect() + } + + /// The number of `Frame` events queued in `node`'s inbox. + fn frame_count(world: &Rc>, 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::>::new()); + assert_eq!(inbox(&world, 1), Vec::>::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()); + } +}