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}