Skip to content

Commit 9284e75

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 9284e75

2 files changed

Lines changed: 43 additions & 20 deletions

File tree

test/antithesis/harness/src/driver.rs

Lines changed: 42 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,26 @@
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;
1115
use std::thread::{self, sleep};
1216
use std::time::{Duration, Instant};
1317

1418
use antithesis_sdk::random::{random_choice, AntithesisRng};
19+
use anyhow::Context;
1520
use rand::{rand_core::UnwrapErr, RngExt};
1621

1722
use crate::payload::dogstatsd;
1823

24+
const SEND_RETRY_BUDGET: Duration = Duration::from_secs(5);
25+
const SEND_RETRY_BACKOFF: Duration = Duration::from_millis(1);
26+
1927
/// Per-batch composition: 50% clean, 25% feral, 25% mixed.
2028
#[derive(Clone, Copy, Debug)]
2129
pub enum Batch {
@@ -82,17 +90,13 @@ pub struct Stats {
8290
pub max_packed: Vec<usize>,
8391
}
8492

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.
93+
/// Drive one batch of sampled `DogStatsD` lines to every socket, blocking through
94+
/// backpressure so on success `sent[i] == received` for all `i`.
9095
///
91-
/// # Panics
96+
/// # Errors
9297
///
93-
/// Panics if the producer or consumer thread panics.
94-
#[must_use]
95-
pub fn run(batch: Batch, sockets: Vec<UnixDatagram>) -> Stats {
98+
/// Errors if a line cannot be delivered within the retry budget, or a worker panics.
99+
pub fn run(batch: Batch, sockets: Vec<UnixDatagram>) -> anyhow::Result<Stats> {
96100
let count = {
97101
let mut rng = UnwrapErr(AntithesisRng);
98102
rng.random_range(0..=10_000u64)
@@ -114,30 +118,49 @@ pub fn run(batch: Batch, sockets: Vec<UnixDatagram>) -> Stats {
114118
}
115119
});
116120

117-
let consumer = thread::spawn(move || {
121+
let consumer = thread::spawn(move || -> anyhow::Result<Stats> {
118122
let mut received = 0usize;
119123
let mut sent = vec![0usize; sockets.len()];
120124
let mut max_packed = vec![0usize; sockets.len()];
121125
while let Ok(line) = rx.recv() {
122126
received += 1;
123127
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);
128-
}
128+
deliver(socket, line.bytes())?;
129+
sent[i] += 1;
130+
if let Line::Multi { count, .. } = &line {
131+
max_packed[i] = max_packed[i].max(*count);
129132
}
130133
}
131134
}
132-
Stats {
135+
Ok(Stats {
133136
received,
134137
sent,
135138
max_packed,
136-
}
139+
})
137140
});
138141

139-
producer.join().expect("producer thread panicked");
140-
consumer.join().expect("consumer thread panicked")
142+
producer
143+
.join()
144+
.map_err(|_| anyhow::anyhow!("producer thread panicked"))?;
145+
consumer
146+
.join()
147+
.map_err(|_| anyhow::anyhow!("consumer thread panicked"))?
148+
}
149+
150+
fn deliver(socket: &UnixDatagram, bytes: &[u8]) -> anyhow::Result<()> {
151+
let deadline = Instant::now() + SEND_RETRY_BUDGET;
152+
loop {
153+
match socket.send(bytes) {
154+
Ok(_) => return Ok(()),
155+
Err(e) if is_transient(&e) && Instant::now() < deadline => sleep(SEND_RETRY_BACKOFF),
156+
Err(e) => return Err(e).context("deliver line to socket"),
157+
}
158+
}
159+
}
160+
161+
fn is_transient(error: &std::io::Error) -> bool {
162+
const ENOBUFS: i32 = 105;
163+
matches!(error.kind(), ErrorKind::WouldBlock | ErrorKind::Interrupted) || error.raw_os_error() == Some(ENOBUFS)
141164
}
142165

143166
/// 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)