1use std::cmp::Reverse;
17use std::collections::BinaryHeap;
18use std::time::Duration;
19
20use bytes::Bytes;
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
23pub enum Side {
24 Left,
26 Right,
28}
29
30impl Side {
31 #[must_use]
33 pub fn peer(self) -> Self {
34 match self {
35 Self::Left => Self::Right,
36 Self::Right => Self::Left,
37 }
38 }
39}
40
41#[derive(Debug, Clone, Copy, Default)]
46pub struct Faults {
47 pub loss: f64,
49 pub duplicate: f64,
54 pub latency: Duration,
56 pub jitter: Duration,
63}
64
65impl Faults {
66 #[must_use]
68 pub fn losing(loss: f64) -> Self {
69 Self {
70 loss,
71 ..Self::default()
72 }
73 }
74
75 #[must_use]
77 pub fn delayed(latency: Duration) -> Self {
78 Self {
79 latency,
80 ..Self::default()
81 }
82 }
83}
84
85#[derive(Debug, Clone)]
87pub struct Delivery {
88 pub to: Side,
90 pub bytes: Bytes,
93}
94
95#[derive(Debug, PartialEq, Eq)]
96struct Scheduled<I> {
97 at: I,
98 sequence: u64,
102 to: Side,
103 bytes: Bytes,
104}
105
106impl<I: Ord> Ord for Scheduled<I> {
107 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
108 self.at
109 .cmp(&other.at)
110 .then_with(|| self.sequence.cmp(&other.sequence))
111 }
112}
113
114impl<I: Ord> PartialOrd for Scheduled<I> {
115 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
116 Some(self.cmp(other))
117 }
118}
119
120#[derive(Debug)]
122pub struct Link<I = tokio::time::Instant> {
123 faults: Faults,
124 state: u64,
125 sequence: u64,
126 in_flight: BinaryHeap<Reverse<Scheduled<I>>>,
127 dropped: u64,
129}
130
131impl<I> Link<I>
132where
133 I: Copy + Ord + std::ops::Add<Duration, Output = I>,
134{
135 #[must_use]
137 pub fn new(seed: u64, faults: Faults) -> Self {
138 Self {
139 faults,
140 state: seed.wrapping_add(0x9E37_79B9_7F4A_7C15),
142 sequence: 0,
143 in_flight: BinaryHeap::new(),
144 dropped: 0,
145 }
146 }
147
148 #[must_use]
150 pub fn perfect() -> Self {
151 Self::new(0, Faults::default())
152 }
153
154 #[must_use]
156 pub fn dropped(&self) -> u64 {
157 self.dropped
158 }
159
160 #[must_use]
162 pub fn in_flight(&self) -> usize {
163 self.in_flight.len()
164 }
165
166 pub fn send(&mut self, from: Side, bytes: Bytes, now: I) {
168 if self.chance() < self.faults.loss {
169 self.dropped = self.dropped.saturating_add(1);
170 return;
171 }
172 self.schedule(from.peer(), bytes.clone(), now);
173 if self.chance() < self.faults.duplicate {
174 self.schedule(from.peer(), bytes, now);
177 }
178 }
179
180 fn schedule(&mut self, to: Side, bytes: Bytes, now: I) {
181 let delay = self.delay();
182 self.sequence = self.sequence.wrapping_add(1);
183 self.in_flight.push(Reverse(Scheduled {
184 at: now + delay,
185 sequence: self.sequence,
186 to,
187 bytes,
188 }));
189 }
190
191 pub fn take_due(&mut self, now: I) -> Vec<Delivery> {
193 let mut arrived = Vec::new();
194 while let Some(Reverse(next)) = self.in_flight.peek() {
195 if next.at > now {
196 break;
197 }
198 let Some(Reverse(scheduled)) = self.in_flight.pop() else {
199 break;
200 };
201 arrived.push(Delivery {
202 to: scheduled.to,
203 bytes: scheduled.bytes,
204 });
205 }
206 arrived
207 }
208
209 #[must_use]
211 pub fn next_arrival(&self) -> Option<I> {
212 self.in_flight.peek().map(|Reverse(next)| next.at)
213 }
214
215 fn delay(&mut self) -> Duration {
217 if self.faults.jitter.is_zero() {
218 return self.faults.latency;
219 }
220 let spread = self.faults.jitter.as_nanos().min(u128::from(u64::MAX));
221 #[expect(
222 clippy::cast_possible_truncation,
223 reason = "clamped to u64::MAX on the line above"
224 )]
225 let spread = spread as u64;
226 let offset = self.next_u64() % (spread.saturating_mul(2).saturating_add(1));
229 let base = Duration::from_nanos(offset);
230 (self.faults.latency + base).saturating_sub(self.faults.jitter)
231 }
232
233 fn chance(&mut self) -> f64 {
235 #[expect(
237 clippy::cast_precision_loss,
238 reason = "53 bits is exactly what an f64 represents; no precision is lost"
239 )]
240 let value = (self.next_u64() >> 11) as f64;
241 value / 9_007_199_254_740_992.0_f64
244 }
245
246 fn next_u64(&mut self) -> u64 {
248 self.state = self.state.wrapping_add(0x9E37_79B9_7F4A_7C15);
249 let mut z = self.state;
250 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
251 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
252 z ^ (z >> 31)
253 }
254}
255
256#[cfg(test)]
257#[allow(
258 clippy::unwrap_used,
259 clippy::expect_used,
260 clippy::panic,
261 clippy::indexing_slicing
262)]
263mod tests {
264 use super::*;
265 use tokio::time::Instant;
266
267 fn datagram(text: &'static str) -> Bytes {
268 Bytes::from_static(text.as_bytes())
269 }
270
271 #[tokio::test(start_paused = true)]
272 async fn a_perfect_link_delivers_everything_immediately() {
273 let mut link = Link::perfect();
274 let now = Instant::now();
275 link.send(Side::Left, datagram("one"), now);
276 link.send(Side::Right, datagram("two"), now);
277
278 let arrived = link.take_due(now);
279 assert_eq!(arrived.len(), 2);
280 assert_eq!(arrived[0].to, Side::Right, "left's datagram goes right");
281 assert_eq!(arrived[1].to, Side::Left);
282 assert_eq!(link.dropped(), 0);
283 }
284
285 #[tokio::test(start_paused = true)]
286 async fn a_link_that_loses_everything_delivers_nothing() {
287 let mut link = Link::new(1, Faults::losing(1.0));
288 let now = Instant::now();
289 for _ in 0..10u32 {
290 link.send(Side::Left, datagram("x"), now);
291 }
292 assert!(link.take_due(now).is_empty());
293 assert_eq!(link.dropped(), 10);
294 }
295
296 #[tokio::test(start_paused = true)]
297 async fn a_delayed_datagram_does_not_arrive_early() {
298 let mut link = Link::new(1, Faults::delayed(Duration::from_millis(50)));
299 let now = Instant::now();
300 link.send(Side::Left, datagram("x"), now);
301
302 assert!(link.take_due(now).is_empty(), "not yet");
303 assert_eq!(link.next_arrival(), Some(now + Duration::from_millis(50)));
304 assert_eq!(link.take_due(now + Duration::from_millis(50)).len(), 1);
305 }
306
307 #[tokio::test(start_paused = true)]
310 async fn one_seed_replays_one_trace() {
311 let trace = |seed: u64| {
312 let mut link = Link::new(seed, Faults::losing(0.5));
313 let now = Instant::now();
314 let mut delivered = Vec::new();
315 for index in 0..40u32 {
316 link.send(Side::Left, Bytes::from(index.to_string()), now);
317 }
318 for delivery in link.take_due(now) {
319 delivered.push(String::from_utf8_lossy(&delivery.bytes).into_owned());
320 }
321 delivered
322 };
323 assert_eq!(trace(7), trace(7), "one seed, one trace");
324 assert_ne!(
325 trace(7),
326 trace(8),
327 "and different seeds explore different traces, or fuzzing the seed does nothing"
328 );
329 }
330
331 #[tokio::test(start_paused = true)]
334 async fn the_loss_rate_is_about_what_was_asked_for() {
335 let mut link = Link::new(42, Faults::losing(0.25));
336 let now = Instant::now();
337 let total = 4000u32;
338 for _ in 0..total {
339 link.send(Side::Left, datagram("x"), now);
340 }
341 let lost = link.dropped();
342 assert!(
343 (800..1200).contains(&lost),
344 "a quarter of 4000 should be near 1000, got {lost}"
345 );
346 }
347
348 #[tokio::test(start_paused = true)]
351 async fn jitter_lets_a_later_datagram_arrive_first() {
352 let mut link = Link::new(
353 3,
354 Faults {
355 latency: Duration::from_millis(50),
356 jitter: Duration::from_millis(40),
357 ..Faults::default()
358 },
359 );
360 let now = Instant::now();
361 for index in 0..20u32 {
362 link.send(Side::Left, Bytes::from(index.to_string()), now);
363 }
364 let order: Vec<String> = link
365 .take_due(now + Duration::from_millis(200))
366 .into_iter()
367 .map(|delivery| String::from_utf8_lossy(&delivery.bytes).into_owned())
368 .collect();
369 let sent: Vec<String> = (0..20u32).map(|index| index.to_string()).collect();
370 assert_eq!(order.len(), sent.len(), "nothing is lost, only reordered");
371 assert_ne!(order, sent, "with 40ms of jitter something must overtake");
372 }
373
374 #[tokio::test(start_paused = true)]
375 async fn duplication_delivers_a_datagram_twice() {
376 let mut link = Link::new(
377 5,
378 Faults {
379 duplicate: 1.0,
380 ..Faults::default()
381 },
382 );
383 let now = Instant::now();
384 link.send(Side::Left, datagram("x"), now);
385 assert_eq!(
386 link.take_due(now).len(),
387 2,
388 "a duplicating link delivers the same datagram twice"
389 );
390 }
391
392 #[tokio::test(start_paused = true)]
393 async fn datagrams_scheduled_together_arrive_in_the_order_they_were_sent() {
394 let mut link = Link::new(1, Faults::delayed(Duration::from_millis(10)));
397 let now = Instant::now();
398 for index in 0..8u32 {
399 link.send(Side::Left, Bytes::from(index.to_string()), now);
400 }
401 let order: Vec<String> = link
402 .take_due(now + Duration::from_millis(10))
403 .into_iter()
404 .map(|delivery| String::from_utf8_lossy(&delivery.bytes).into_owned())
405 .collect();
406 assert_eq!(order, (0..8u32).map(|i| i.to_string()).collect::<Vec<_>>());
407 }
408}