Skip to main content

recon_sim/
config.rs

1//! What a run does to the messages passing through it.
2
3use core::time::Duration;
4
5/// Network conditions and run limits.
6///
7/// Every knob is consulted through the run's seeded generator, so a configuration plus a seed
8/// determines a run completely.
9#[derive(Debug, Clone, PartialEq)]
10pub struct Config {
11    /// Seed for every random decision in the run — faults, latency, and protocol randomness.
12    pub seed: u64,
13    /// Probability in `0.0..=1.0` that a message is dropped rather than delivered.
14    pub loss: f64,
15    /// Probability in `0.0..=1.0` that a message is delivered twice.
16    pub duplication: f64,
17    /// Probability in `0.0..=1.0` that a message is delayed far beyond normal latency,
18    /// forcing it behind messages sent after it.
19    pub reorder: f64,
20    /// Shortest delivery delay.
21    pub latency_min: Duration,
22    /// Longest delivery delay. Jitter between the two produces ordinary reordering.
23    pub latency_max: Duration,
24    /// Extra delay applied to a message selected for reordering.
25    pub reorder_delay: Duration,
26    /// When set, the run is synchronous: delivery between connected, uncrashed processes is
27    /// guaranteed within this bound, and nothing is lost, duplicated or given a reordering spike.
28    ///
29    /// This is what a perfect failure detector needs and what the asynchronous default cannot
30    /// offer: without a known bound, a live process whose messages are unlucky is
31    /// indistinguishable from a crashed one. It constrains *timing* only — crashes and
32    /// partitions still stop delivery.
33    pub synchronous: Option<Duration>,
34    /// When true, communication happens within sessions: between each pair of processes there is
35    /// a session in which delivery is reliable, ordered and free of duplicates — what TCP or QUIC
36    /// gives. A partition, a crash, or an explicit break ends it, losing an unknown suffix of what
37    /// was in flight, and a new session begins at a higher epoch.
38    ///
39    /// This is the model a deployed stack would run on. The fair-loss default is what you have if
40    /// you build reliability yourself, which is the simulator's own situation and not
41    /// production's.
42    pub sessions: bool,
43    /// How often a session-based run retries establishing sessions that are not up.
44    ///
45    /// A deployed link keeps trying to reconnect on its own rather than waiting for the layers
46    /// above to transmit, so the model does too. The value stands in for a retry interval, with or
47    /// without backoff; no protocol may depend on it.
48    pub reconnect_interval: Duration,
49    /// Safety valve: a run stops after this many events, whatever the clock says.
50    ///
51    /// Protocols such as the stubborn link retransmit forever by design, so a run is bounded
52    /// by time or by this, never by quiescence.
53    pub max_steps: u64,
54}
55
56impl Default for Config {
57    fn default() -> Self {
58        Config {
59            seed: 0,
60            loss: 0.0,
61            duplication: 0.0,
62            reorder: 0.0,
63            latency_min: Duration::from_millis(1),
64            latency_max: Duration::from_millis(1),
65            reorder_delay: Duration::from_millis(50),
66            synchronous: None,
67            sessions: false,
68            reconnect_interval: Duration::from_millis(5),
69            max_steps: 1_000_000,
70        }
71    }
72}
73
74impl Config {
75    pub fn seed(mut self, seed: u64) -> Self {
76        self.seed = seed;
77        self
78    }
79
80    /// Drop messages with probability `p`.
81    pub fn loss(mut self, p: f64) -> Self {
82        self.loss = p;
83        self
84    }
85
86    /// Deliver messages twice with probability `p`.
87    pub fn duplication(mut self, p: f64) -> Self {
88        self.duplication = p;
89        self
90    }
91
92    /// Delay messages far beyond normal latency with probability `p`, forcing reordering.
93    pub fn reorder(mut self, p: f64) -> Self {
94        self.reorder = p;
95        self
96    }
97
98    /// Deliver after a delay drawn uniformly from `min..=max`.
99    ///
100    /// Jitter here is itself a source of reordering; `reorder` forces the extreme case.
101    pub fn latency(mut self, min: Duration, max: Duration) -> Self {
102        self.latency_min = min;
103        self.latency_max = max;
104        self
105    }
106
107    /// Run synchronously: every message between connected, uncrashed processes is delivered
108    /// within `bound`, and none is lost or duplicated.
109    ///
110    /// The bound is readable afterwards through [`Config::delivery_bound`], so a protocol whose
111    /// correctness depends on it can be configured from the same value rather than from a guess.
112    /// Setting this overrides the fault knobs, and the override is enforced at delivery time —
113    /// calling `loss` afterwards will not quietly reintroduce loss.
114    pub fn synchronous(mut self, bound: Duration) -> Self {
115        self.synchronous = Some(bound);
116        self.loss = 0.0;
117        self.duplication = 0.0;
118        self.reorder = 0.0;
119        self.latency_max = bound;
120        if self.latency_min > bound {
121            self.latency_min = bound;
122        }
123        self
124    }
125
126    /// The upper bound on delivery, when the run is synchronous.
127    pub fn delivery_bound(&self) -> Option<Duration> {
128        self.synchronous
129    }
130
131    /// Whether this run makes a timing guarantee.
132    pub fn is_synchronous(&self) -> bool {
133        self.synchronous.is_some()
134    }
135
136    /// Communicate within sessions: reliable, ordered, duplicate-free delivery while a session
137    /// holds, and an unknown lost suffix when one ends.
138    ///
139    /// Overrides the loss and duplication knobs, enforced at delivery time so builder order
140    /// cannot reintroduce them. Latency still applies, but a message is never delivered before one
141    /// sent earlier to the same peer.
142    pub fn sessions(mut self) -> Self {
143        self.sessions = true;
144        self.loss = 0.0;
145        self.duplication = 0.0;
146        self.reorder = 0.0;
147        self
148    }
149
150    /// How often to retry establishing sessions that are not up.
151    pub fn reconnect_interval(mut self, d: Duration) -> Self {
152        self.reconnect_interval = d;
153        self
154    }
155
156    /// Whether this run communicates within sessions.
157    pub fn is_session_based(&self) -> bool {
158        self.sessions
159    }
160
161    pub fn max_steps(mut self, n: u64) -> Self {
162        self.max_steps = n;
163        self
164    }
165}