Skip to content

Commit a23df6b

Browse files
committed
chore(antithesis): Parallel drivers retry sends
We modify parallel driver to tolerate both backpressure and network faults. The goal of this work is to ensure we do not delver to one SUT and not the other. All payloads rip through or the driver fails loudly.
1 parent 1d0f918 commit a23df6b

4 files changed

Lines changed: 82 additions & 21 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/antithesis/harness/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ clap = { workspace = true, features = [
1717
"usage",
1818
] }
1919
itoa = { workspace = true }
20+
libc = { workspace = true }
2021
num-traits = { workspace = true }
2122
rand = { workspace = true }
2223
ryu = { workspace = true }

test/antithesis/harness/src/driver.rs

Lines changed: 74 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@
44
//! fans each line out to every socket and tallies per-socket sends. Drivers
55
//! differ only in how many sockets they target and which anchors they fire, so
66
//! 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.
710
11+
use std::io::ErrorKind;
812
use std::os::unix::net::UnixDatagram;
913
use std::path::Path;
1014
use std::sync::mpsc::sync_channel;
@@ -16,6 +20,9 @@ use rand::{rand_core::UnwrapErr, RngExt};
1620

1721
use crate::payload::dogstatsd;
1822

23+
const SEND_RETRY_BUDGET: Duration = Duration::from_secs(5);
24+
const SEND_RETRY_BACKOFF: Duration = Duration::from_millis(1);
25+
1926
/// Per-batch composition: 50% clean, 25% feral, 25% mixed.
2027
#[derive(Clone, Copy, Debug)]
2128
pub enum Batch {
@@ -80,19 +87,19 @@ pub struct Stats {
8087
/// Largest packed run that reached each socket, indexed likewise. Zero when
8188
/// no multi-value line reached that socket.
8289
pub max_packed: Vec<usize>,
90+
/// Whether a send exhausted the retry budget under sustained backpressure.
91+
/// Distinguishes a wedged or paused peer from a clean partial batch.
92+
pub timed_out: bool,
8393
}
8494

85-
/// Drive one batch of sampled `DogStatsD` lines to every socket.
86-
///
87-
/// A producer samples up to ~10k lines at the batch's vibe and queues them; a
88-
/// consumer ships each line to every socket and tallies per-socket sends. The
89-
/// returned [`Stats`] anchor the caller's assertions.
95+
/// Drive one batch of sampled `DogStatsD` lines to every socket, blocking through
96+
/// backpressure so on success `sent[i] == received` for all `i`.
9097
///
91-
/// # Panics
98+
/// # Errors
9299
///
93-
/// Panics if the producer or consumer thread panics.
94-
#[must_use]
95-
pub fn run(batch: Batch, sockets: Vec<UnixDatagram>) -> Stats {
100+
/// Errors if a worker thread panics. Sustained backpressure past the retry budget
101+
/// is reported via [`Stats::timed_out`], not as an error.
102+
pub fn run(batch: Batch, sockets: Vec<UnixDatagram>) -> anyhow::Result<Stats> {
96103
let count = {
97104
let mut rng = UnwrapErr(AntithesisRng);
98105
rng.random_range(0..=10_000u64)
@@ -114,30 +121,78 @@ pub fn run(batch: Batch, sockets: Vec<UnixDatagram>) -> Stats {
114121
}
115122
});
116123

117-
let consumer = thread::spawn(move || {
124+
let consumer = thread::spawn(move || -> anyhow::Result<Stats> {
118125
let mut received = 0usize;
119126
let mut sent = vec![0usize; sockets.len()];
120127
let mut max_packed = vec![0usize; sockets.len()];
121-
while let Ok(line) = rx.recv() {
128+
let mut timed_out = false;
129+
'recv: while let Ok(line) = rx.recv() {
122130
received += 1;
123131
for (i, socket) in sockets.iter().enumerate() {
124-
if socket.send(line.bytes()).is_ok() {
125-
sent[i] += 1;
126-
if let Line::Multi { count, .. } = &line {
127-
max_packed[i] = max_packed[i].max(*count);
132+
match deliver(socket, line.bytes()) {
133+
Delivery::Sent => {
134+
sent[i] += 1;
135+
if let Line::Multi { count, .. } = &line {
136+
max_packed[i] = max_packed[i].max(*count);
137+
}
138+
}
139+
// Peer left mid-batch after Antithesis killed the SUT. Stop and
140+
// report the partial batch rather than failing the run.
141+
Delivery::Unavailable => break 'recv,
142+
// Backpressure outlasted the retry budget. A legit Antithesis
143+
// pause reaches here, so record it and stop rather than fail.
144+
Delivery::Timeout => {
145+
timed_out = true;
146+
break 'recv;
128147
}
129148
}
130149
}
131150
}
132-
Stats {
151+
Ok(Stats {
133152
received,
134153
sent,
135154
max_packed,
136-
}
155+
timed_out,
156+
})
137157
});
138158

139-
producer.join().expect("producer thread panicked");
140-
consumer.join().expect("consumer thread panicked")
159+
producer
160+
.join()
161+
.map_err(|_| anyhow::anyhow!("producer thread panicked"))?;
162+
consumer
163+
.join()
164+
.map_err(|_| anyhow::anyhow!("consumer thread panicked"))?
165+
}
166+
167+
/// Outcome of delivering one line to a socket.
168+
enum Delivery {
169+
/// The line reached the socket.
170+
Sent,
171+
/// The peer is gone. Stop the batch and report the partial result.
172+
Unavailable,
173+
/// Backpressure outlasted the retry budget. Fail the run.
174+
Timeout,
175+
}
176+
177+
fn deliver(socket: &UnixDatagram, bytes: &[u8]) -> Delivery {
178+
let deadline = Instant::now() + SEND_RETRY_BUDGET;
179+
loop {
180+
match socket.send(bytes) {
181+
Ok(_) => return Delivery::Sent,
182+
Err(e) if is_transient(&e) => {
183+
if Instant::now() >= deadline {
184+
return Delivery::Timeout;
185+
}
186+
sleep(SEND_RETRY_BACKOFF);
187+
}
188+
Err(_) => return Delivery::Unavailable,
189+
}
190+
}
191+
}
192+
193+
fn is_transient(error: &std::io::Error) -> bool {
194+
matches!(error.kind(), ErrorKind::WouldBlock | ErrorKind::Interrupted)
195+
|| error.raw_os_error() == Some(libc::ENOBUFS)
141196
}
142197

143198
/// Wait for the remote process to bind `path`, intentionally naive. Returns

test/antithesis/scenarios/general/src/bin/parallel_driver_send_dogstatsd.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,17 @@ mod unix_driver {
3333
};
3434

3535
let batch = Batch::sample();
36-
let stats = driver::run(batch, vec![socket]);
36+
let stats = driver::run(batch, vec![socket])?;
3737
let sent = stats.sent[0];
3838
let max_packed = stats.max_packed[0];
3939

4040
assert_reachable!(
4141
"workload ran a dogstatsd batch",
42-
&json!({ "sent": sent, "dogstatsd_socket": config.dogstatsd_socket.display().to_string() })
42+
&json!({
43+
"sent": sent,
44+
"timed_out": stats.timed_out,
45+
"dogstatsd_socket": config.dogstatsd_socket.display().to_string()
46+
})
4347
);
4448
assert_sometimes!(sent > 0, "workload sent a dogstatsd line", &json!({ "sent": sent }));
4549
assert_sometimes!(

0 commit comments

Comments
 (0)