1use std::collections::{BTreeMap, BTreeSet};
44use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
45use std::time::Duration;
46
47use crate::entropy::{Entropy, SeededEntropy};
48
49pub(crate) const NET_STREAM_SALT: u64 = 0x4E37_5EED_4E37_5EED;
52
53#[non_exhaustive]
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub enum NetFault {
57 None,
59 Dropped,
62 Partitioned,
64 TimedOut,
67}
68
69impl std::fmt::Display for NetFault {
70 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71 f.write_str(match self {
72 Self::None => "delivered",
73 Self::Dropped => "dropped",
74 Self::Partitioned => "partitioned",
75 Self::TimedOut => "timed out",
76 })
77 }
78}
79
80#[non_exhaustive]
82#[derive(Debug, Clone, PartialEq, Eq)]
83pub struct NetEvent {
84 pub seq: u64,
86 pub host: String,
88 pub latency: Duration,
90 pub fault: NetFault,
92}
93
94#[derive(Clone, Default)]
100pub struct SimNet {
101 inner: Arc<Mutex<NetState>>,
102}
103
104struct NetState {
105 hosts: BTreeMap<String, axum::Router>,
106 partitioned: BTreeSet<String>,
107 drop_rate: f64,
108 latency: (Duration, Duration),
109 stream: Arc<dyn Entropy>,
110 events: Vec<NetEvent>,
111}
112
113impl Default for NetState {
114 fn default() -> Self {
115 Self {
116 hosts: BTreeMap::new(),
117 partitioned: BTreeSet::new(),
118 drop_rate: 0.0,
119 latency: (Duration::ZERO, Duration::ZERO),
120 stream: SeededEntropy::shared(NET_STREAM_SALT),
121 events: Vec::new(),
122 }
123 }
124}
125
126impl std::fmt::Debug for SimNet {
127 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
128 let state = self.lock();
129 f.debug_struct("SimNet")
130 .field("hosts", &state.hosts.keys().collect::<Vec<_>>())
131 .field("partitioned", &state.partitioned)
132 .field("drop_rate", &state.drop_rate)
133 .field("latency", &state.latency)
134 .finish_non_exhaustive()
135 }
136}
137
138impl SimNet {
139 #[must_use]
141 pub fn new() -> Self {
142 Self::default()
143 }
144
145 #[must_use]
148 pub fn host(self, host: impl Into<String>, router: axum::Router) -> Self {
149 self.lock()
150 .hosts
151 .insert(canonical_host(&host.into()), router);
152 self
153 }
154
155 #[must_use]
158 pub fn latency(self, min: Duration, max: Duration) -> Self {
159 self.lock().latency = (min, max.max(min));
160 self
161 }
162
163 #[must_use]
166 pub fn drop_rate(self, rate: f64) -> Self {
167 self.lock().drop_rate = super::chaos::clamp_prob(rate);
168 self
169 }
170
171 pub fn partition(&self, host: impl Into<String>) {
174 self.lock().partitioned.insert(canonical_host(&host.into()));
175 }
176
177 pub fn heal(&self, host: &str) {
179 self.lock().partitioned.remove(&canonical_host(host));
180 }
181
182 #[must_use]
185 pub fn events(&self) -> Vec<NetEvent> {
186 self.lock().events.clone()
187 }
188
189 pub(crate) fn reseed(&self, seed: u64) {
191 self.lock().stream = SeededEntropy::shared(seed ^ NET_STREAM_SALT);
192 }
193
194 pub(crate) fn insert_host(&self, host: &str, router: axum::Router) {
196 self.lock().hosts.insert(canonical_host(host), router);
197 }
198
199 pub(crate) fn remove_host(&self, host: &str) {
201 self.lock().hosts.remove(&canonical_host(host));
202 }
203
204 pub(crate) fn service(&self, host: &str) -> Option<axum::Router> {
206 self.lock().hosts.get(&canonical_host(host)).cloned()
207 }
208
209 pub(crate) async fn transmit(
213 &self,
214 host: &str,
215 timeout: Option<Duration>,
216 ) -> Result<(), NetFault> {
217 let (latency, fault) = {
218 let state = self.lock();
219 let latency_draw = state.stream.next_u64();
222 let drop_draw = state.stream.next_u64();
223 let (latency, fault) = if state.partitioned.contains(&canonical_host(host)) {
224 (Duration::ZERO, NetFault::Partitioned)
225 } else if super::chaos::unit_from_draw(drop_draw) < state.drop_rate {
226 (
227 sample_latency(state.latency, latency_draw),
228 NetFault::Dropped,
229 )
230 } else {
231 (sample_latency(state.latency, latency_draw), NetFault::None)
232 };
233 let (latency, fault) = match timeout {
234 Some(limit) if fault != NetFault::Partitioned && latency > limit => {
235 (limit, NetFault::TimedOut)
236 }
237 _ => (latency, fault),
238 };
239 drop(state);
240 (latency, fault)
241 };
242 if !latency.is_zero() {
243 tokio::time::sleep(latency).await;
244 }
245 {
250 let mut state = self.lock();
251 let seq = state.events.len() as u64;
252 state.events.push(NetEvent {
253 seq,
254 host: host.to_owned(),
255 latency,
256 fault,
257 });
258 }
259 match fault {
260 NetFault::None => Ok(()),
261 fault => Err(fault),
262 }
263 }
264
265 fn lock(&self) -> MutexGuard<'_, NetState> {
266 self.inner.lock().unwrap_or_else(PoisonError::into_inner)
267 }
268}
269
270fn sample_latency((min, max): (Duration, Duration), draw: u64) -> Duration {
273 let min_ns = u64::try_from(min.as_nanos()).unwrap_or(u64::MAX);
274 let max_ns = u64::try_from(max.as_nanos()).unwrap_or(u64::MAX);
275 let span = max_ns.saturating_sub(min_ns).saturating_add(1);
276 Duration::from_nanos(min_ns.saturating_add(draw % span))
277}
278
279pub(crate) fn canonical_host(host: &str) -> String {
283 url::Host::parse(host).map_or_else(|_| host.to_ascii_lowercase(), |parsed| parsed.to_string())
284}
285
286#[cfg(test)]
287mod tests {
288 #[test]
289 fn a_host_name_matches_as_a_url_host_does() {
290 use super::canonical_host;
291 assert_eq!(canonical_host("Payments"), "payments");
292 assert_eq!(
293 canonical_host("PAYMENTS.Example.COM"),
294 "payments.example.com"
295 );
296 assert_eq!(canonical_host("[::1]"), "[::1]");
297 assert_eq!(canonical_host("10.0.0.1"), "10.0.0.1");
298 let url = url::Url::parse("http://Bücher.Example/").unwrap();
299 assert_eq!(canonical_host("Bücher.Example"), url.host_str().unwrap());
300 }
301
302 use super::{Duration, NetFault, SimNet, sample_latency};
303
304 #[test]
305 fn latency_stays_in_range() {
306 let range = (Duration::from_millis(5), Duration::from_millis(9));
307 for draw in 0..100 {
308 let latency = sample_latency(range, draw);
309 assert!(latency >= range.0 && latency <= range.1, "{latency:?}");
310 }
311 let fixed = (Duration::from_millis(7), Duration::from_millis(7));
312 assert_eq!(sample_latency(fixed, u64::MAX), Duration::from_millis(7));
313 }
314
315 #[test]
316 fn sub_millisecond_latency_is_kept() {
317 let fixed = (Duration::from_micros(500), Duration::from_micros(500));
318 assert_eq!(sample_latency(fixed, 12_345), Duration::from_micros(500));
319 let range = (Duration::from_micros(1500), Duration::from_micros(2500));
320 for draw in [0, 1, 999, 1000, 1_000_000, u64::MAX] {
321 let latency = sample_latency(range, draw);
322 assert!(latency >= range.0 && latency <= range.1, "{latency:?}");
323 }
324 }
325
326 #[tokio::test(start_paused = true)]
327 async fn a_latency_past_the_timeout_is_recorded_as_a_timeout() {
328 let net = SimNet::new().latency(Duration::from_secs(60), Duration::from_secs(60));
329 let limit = Duration::from_secs(30);
330 let start = tokio::time::Instant::now();
331 assert_eq!(
332 net.transmit("h", Some(limit)).await,
333 Err(NetFault::TimedOut)
334 );
335 assert_eq!(start.elapsed(), limit, "only the timeout passes");
336 let events = net.events();
337 assert_eq!(events[0].fault, NetFault::TimedOut);
338 assert_eq!(events[0].latency, limit);
339 assert_eq!(
340 net.transmit("h", None).await,
341 Ok(()),
342 "no timeout, no fault"
343 );
344 }
345
346 #[test]
347 fn max_below_min_is_raised() {
348 let net = SimNet::new().latency(Duration::from_millis(9), Duration::from_millis(1));
349 assert_eq!(
350 net.lock().latency,
351 (Duration::from_millis(9), Duration::from_millis(9))
352 );
353 }
354
355 #[tokio::test(start_paused = true)]
356 async fn partition_fails_at_once_and_heal_restores() {
357 let net = SimNet::new();
358 net.partition("a");
359 assert_eq!(net.transmit("a", None).await, Err(NetFault::Partitioned));
360 net.heal("a");
361 assert_eq!(net.transmit("a", None).await, Ok(()));
362 let faults: Vec<_> = net.events().iter().map(|event| event.fault).collect();
363 assert_eq!(faults, vec![NetFault::Partitioned, NetFault::None]);
364 }
365
366 #[tokio::test(start_paused = true)]
367 async fn an_attempt_dropped_during_its_latency_leaves_no_event() {
368 let net = SimNet::new().latency(Duration::from_millis(10), Duration::from_millis(10));
369 let outcome = crate::sim::crash_at(0, net.transmit("h", None)).await;
370 assert!(outcome.is_crashed());
371 assert!(net.events().is_empty(), "the host was never reached");
372 assert_eq!(net.transmit("h", None).await, Ok(()));
373 assert_eq!(net.events().len(), 1);
374 assert_eq!(net.events()[0].seq, 0);
375 }
376
377 #[tokio::test(start_paused = true)]
378 async fn same_seed_same_decisions() {
379 let run = |seed| async move {
380 let net = SimNet::new()
381 .drop_rate(0.5)
382 .latency(Duration::ZERO, Duration::from_millis(30));
383 net.reseed(seed);
384 for _ in 0..32 {
385 let _ = net.transmit("h", None).await;
386 }
387 net.events()
388 };
389 assert_eq!(run(1).await, run(1).await);
390 assert_ne!(run(1).await, run(2).await);
391 }
392}