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 ;
812use std:: os:: unix:: net:: UnixDatagram ;
913use std:: path:: Path ;
1014use std:: sync:: mpsc:: sync_channel;
@@ -16,6 +20,9 @@ use rand::{rand_core::UnwrapErr, RngExt};
1620
1721use 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 ) ]
2128pub 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
0 commit comments