Skip to content

Commit 345a39c

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 345a39c

4 files changed

Lines changed: 58 additions & 20 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: 55 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 {
@@ -82,17 +89,13 @@ pub struct Stats {
8289
pub max_packed: Vec<usize>,
8390
}
8491

85-
/// Drive one batch of sampled `DogStatsD` lines to every socket.
92+
/// Drive one batch of sampled `DogStatsD` lines to every socket, blocking through
93+
/// backpressure so on success `sent[i] == received` for all `i`.
8694
///
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+
/// # Errors
9096
///
91-
/// # Panics
92-
///
93-
/// Panics if the producer or consumer thread panics.
94-
#[must_use]
95-
pub fn run(batch: Batch, sockets: Vec<UnixDatagram>) -> Stats {
97+
/// Errors if a line cannot be delivered within the retry budget, or a worker panics.
98+
pub fn run(batch: Batch, sockets: Vec<UnixDatagram>) -> anyhow::Result<Stats> {
9699
let count = {
97100
let mut rng = UnwrapErr(AntithesisRng);
98101
rng.random_range(0..=10_000u64)
@@ -114,30 +117,63 @@ pub fn run(batch: Batch, sockets: Vec<UnixDatagram>) -> Stats {
114117
}
115118
});
116119

117-
let consumer = thread::spawn(move || {
120+
let consumer = thread::spawn(move || -> anyhow::Result<Stats> {
118121
let mut received = 0usize;
119122
let mut sent = vec![0usize; sockets.len()];
120123
let mut max_packed = vec![0usize; sockets.len()];
121-
while let Ok(line) = rx.recv() {
124+
'recv: while let Ok(line) = rx.recv() {
122125
received += 1;
123126
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);
127+
match deliver(socket, line.bytes()) {
128+
Delivery::Sent => {
129+
sent[i] += 1;
130+
if let Line::Multi { count, .. } = &line {
131+
max_packed[i] = max_packed[i].max(*count);
132+
}
128133
}
134+
// Peer left mid-batch after Antithesis killed the SUT. Stop and
135+
// report the partial batch rather than failing the run.
136+
Delivery::Unavailable => break 'recv,
129137
}
130138
}
131139
}
132-
Stats {
140+
Ok(Stats {
133141
received,
134142
sent,
135143
max_packed,
136-
}
144+
})
137145
});
138146

139-
producer.join().expect("producer thread panicked");
140-
consumer.join().expect("consumer thread panicked")
147+
producer
148+
.join()
149+
.map_err(|_| anyhow::anyhow!("producer thread panicked"))?;
150+
consumer
151+
.join()
152+
.map_err(|_| anyhow::anyhow!("consumer thread panicked"))?
153+
}
154+
155+
/// Outcome of delivering one line to a socket.
156+
enum Delivery {
157+
/// The line reached the socket.
158+
Sent,
159+
/// The peer is gone or backpressure outlasted the retry budget. Stop the batch.
160+
Unavailable,
161+
}
162+
163+
fn deliver(socket: &UnixDatagram, bytes: &[u8]) -> Delivery {
164+
let deadline = Instant::now() + SEND_RETRY_BUDGET;
165+
loop {
166+
match socket.send(bytes) {
167+
Ok(_) => return Delivery::Sent,
168+
Err(e) if is_transient(&e) && Instant::now() < deadline => sleep(SEND_RETRY_BACKOFF),
169+
Err(_) => return Delivery::Unavailable,
170+
}
171+
}
172+
}
173+
174+
fn is_transient(error: &std::io::Error) -> bool {
175+
matches!(error.kind(), ErrorKind::WouldBlock | ErrorKind::Interrupted)
176+
|| error.raw_os_error() == Some(libc::ENOBUFS)
141177
}
142178

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

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ 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

0 commit comments

Comments
 (0)