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}