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 {
@@ -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
0 commit comments