harness/driver.rs
1//! Shared `DogStatsD` load-driver engine.
2//!
3//! A producer thread samples lines into a bounded channel; a consumer thread
4//! fans each line out to every socket and tallies per-socket sends. Drivers
5//! differ only in how many sockets they target and which anchors they fire, so
6//! both the single-socket and differential drivers run on this one engine.
7//!
8//! NOTE: this driver intentionally blocks on backpressure from the SUT. Retry
9//! and backoff timers are meant to endure transient errors.
10
11use std::io::ErrorKind;
12use std::os::unix::net::UnixDatagram;
13use std::path::Path;
14use std::sync::mpsc::sync_channel;
15use std::thread::{self, sleep};
16use std::time::{Duration, Instant};
17
18use rand::seq::IndexedRandom;
19use rand::Rng;
20
21use crate::payload::dogstatsd;
22pub use crate::payload::dogstatsd::Batch;
23
24const SEND_RETRY_BUDGET: Duration = Duration::from_secs(5);
25const SEND_RETRY_BACKOFF: Duration = Duration::from_millis(1);
26
27/// Sample a line composition: half clean, a quarter feral, a quarter mixed.
28#[must_use]
29pub fn sample<R: Rng + ?Sized>(rng: &mut R) -> Batch {
30 match [Batch::Clean, Batch::Clean, Batch::Feral, Batch::Mixed].choose(rng) {
31 Some(Batch::Feral) => Batch::Feral,
32 Some(Batch::Mixed) => Batch::Mixed,
33 _ => Batch::Clean,
34 }
35}
36
37/// A generated payload queued for the sockets: the packed bytes and what they hold.
38struct Datagram {
39 /// The `\n`-packed payload bytes to ship over a socket.
40 bytes: Vec<u8>,
41 /// The lines and largest packed run in `bytes`.
42 payload: dogstatsd::Payload,
43}
44
45/// What a driver run shipped, for anchoring assertions.
46#[derive(Clone, Debug)]
47pub struct Stats {
48 /// Payloads pulled from the channel, whether or not any send succeeded.
49 pub received: usize,
50 /// Lines delivered per socket, summed across payloads, indexed as the sockets
51 /// were passed to [`run`].
52 pub sent: Vec<usize>,
53 /// Largest packed run that reached each socket, indexed likewise. Zero when
54 /// no multi-value line reached that socket.
55 pub max_packed: Vec<usize>,
56 /// Whether a send exhausted the retry budget under sustained backpressure.
57 /// Distinguishes a wedged or paused peer from a clean partial batch.
58 pub timed_out: bool,
59}
60
61/// Drive `count` sampled `DogStatsD` datagrams to every socket, packing each to
62/// at most `limit_bytes` and blocking through transient backpressure so every
63/// datagram reaches every socket. Both `count` and `limit_bytes` come from a load
64/// generator's [`crate::config::DriverConfig`], so a datagram never truncates on
65/// receive.
66///
67/// A peer that leaves mid-batch, or backpressure that outlasts the retry budget,
68/// ends the run early with a partial [`Stats`] rather than an error.
69///
70/// # Errors
71///
72/// Errors if a worker thread panics. Sustained backpressure is reported via
73/// [`Stats::timed_out`], not as an error.
74pub fn run<R: Rng + Send + 'static>(
75 mut rng: R, batch: Batch, limit_bytes: usize, count: usize, sockets: Vec<UnixDatagram>,
76) -> anyhow::Result<Stats> {
77 let (tx, rx) = sync_channel::<Datagram>(2024);
78
79 let producer = thread::spawn(move || {
80 for _ in 0..count {
81 let mut bytes = Vec::new();
82 let payload = dogstatsd::write_payload(&mut rng, &mut bytes, batch, limit_bytes);
83 if tx.send(Datagram { bytes, payload }).is_err() {
84 break;
85 }
86 }
87 });
88
89 let consumer = thread::spawn(move || -> anyhow::Result<Stats> {
90 let mut received = 0usize;
91 let mut sent = vec![0usize; sockets.len()];
92 let mut max_packed = vec![0usize; sockets.len()];
93 let mut timed_out = false;
94 'recv: while let Ok(datagram) = rx.recv() {
95 received += 1;
96 for (i, socket) in sockets.iter().enumerate() {
97 match deliver(socket, &datagram.bytes) {
98 Delivery::Sent => {
99 sent[i] += datagram.payload.lines;
100 max_packed[i] = max_packed[i].max(datagram.payload.max_packed);
101 }
102 // Peer left mid-batch after Antithesis killed the SUT. Stop and
103 // report the partial batch rather than failing the run.
104 Delivery::Unavailable => break 'recv,
105 // Backpressure outlasted the retry budget. A legit Antithesis
106 // pause reaches here, so record it and stop rather than fail.
107 Delivery::Timeout => {
108 timed_out = true;
109 break 'recv;
110 }
111 }
112 }
113 }
114 Ok(Stats {
115 received,
116 sent,
117 max_packed,
118 timed_out,
119 })
120 });
121
122 producer
123 .join()
124 .map_err(|_| anyhow::anyhow!("producer thread panicked"))?;
125 consumer
126 .join()
127 .map_err(|_| anyhow::anyhow!("consumer thread panicked"))?
128}
129
130/// Outcome of delivering one line to a socket.
131enum Delivery {
132 /// The line reached the socket.
133 Sent,
134 /// The peer is gone. Stop the batch and report the partial result.
135 Unavailable,
136 /// Backpressure outlasted the retry budget. Fail the run.
137 Timeout,
138}
139
140fn deliver(socket: &UnixDatagram, bytes: &[u8]) -> Delivery {
141 let deadline = Instant::now() + SEND_RETRY_BUDGET;
142 loop {
143 match socket.send(bytes) {
144 Ok(_) => return Delivery::Sent,
145 Err(e) if is_transient(&e) => {
146 if Instant::now() >= deadline {
147 return Delivery::Timeout;
148 }
149 sleep(SEND_RETRY_BACKOFF);
150 }
151 Err(_) => return Delivery::Unavailable,
152 }
153 }
154}
155
156fn is_transient(error: &std::io::Error) -> bool {
157 matches!(error.kind(), ErrorKind::WouldBlock | ErrorKind::Interrupted)
158 || error.raw_os_error() == Some(libc::ENOBUFS)
159}
160
161/// Wait for the remote process to bind `path`, intentionally naive. Returns
162/// `None` if the socket is still unavailable after 30 seconds.
163#[must_use]
164pub fn connect_with_retry(path: &Path) -> Option<UnixDatagram> {
165 let deadline = Instant::now() + Duration::from_secs(30);
166 loop {
167 if let Ok(socket) = UnixDatagram::unbound() {
168 if socket.connect(path).is_ok() {
169 return Some(socket);
170 }
171 }
172 if Instant::now() >= deadline {
173 return None;
174 }
175 sleep(Duration::from_millis(250));
176 }
177}