@@ -17,14 +17,13 @@ use crate::traits::{AggregateSink, FlushableSink, RootSink};
1717// have the optional `shuttle` crate linked at all.
1818//
1919// `RecvTimeoutError` needs no swap -- shuttle re-exports std's type
20- // unchanged. Its `recv_timeout` never actually times out though,
21- // that's a known gap documented in Shuttle itself:
22- // https://github.com/awslabs/shuttle/blob/c8a46d3965048df3207ec920dae066bc9c4d9d89/shuttle-std/src/sync/mpsc.rs#L433
20+ // unchanged. Its `recv_timeout` never actually times out though, that's a
21+ // known gap documented in Shuttle itself; `channel()`'s shuttle-side
22+ // substitute (in `metrique_writer_core::shuttle_test_support`, shared with
23+ // sibling crates) wraps the receiver half so it randomly synthesizes a
24+ // `Timeout` instead.
2325#[ cfg( all( shuttle, feature = "_shuttle" ) ) ]
24- use shuttle:: {
25- sync:: mpsc:: { Sender , channel} ,
26- thread,
27- } ;
26+ use metrique_writer_core:: shuttle_test_support:: { Sender , channel, thread} ;
2827#[ cfg( not( all( shuttle, feature = "_shuttle" ) ) ) ]
2928use std:: {
3029 sync:: mpsc:: { Sender , channel} ,
@@ -217,12 +216,8 @@ mod tests {
217216 }
218217}
219218
220- // Shuttle interleaving tests for `WorkerSink`, covering merge correctness
221- // and clean exit on channel disconnect.
222- //
223- // Since `recv_timeout` never actually times out under shuttle (see the
224- // primitives import comment above), these tests don't exercise the
225- // periodic-flush path.
219+ // Shuttle interleaving tests for `WorkerSink`, covering merge correctness,
220+ // clean exit on channel disconnect, and the periodic-flush-on-timeout path.
226221#[ cfg( all( test, shuttle, feature = "_shuttle" ) ) ]
227222mod shuttle_tests {
228223 use std:: sync:: Mutex ;
@@ -307,11 +302,10 @@ mod shuttle_tests {
307302 . expect ( "worker thread panicked" ) ;
308303 }
309304
310- /// The historical bug, reproduced directly: several cloned handles send
311- /// an entry and drop concurrently. The background thread must still
312- /// exit (this used to hang forever -- see the module doc comment),
313- /// flush exactly once at shutdown, and lose no entries, no matter how
314- /// the drops and the final disconnect interleave.
305+ /// Several cloned handles send an entry and drop concurrently.
306+ /// The background thread must still exit, flush at least once,
307+ /// and lose no entries, no matter how the drops, the final disconnect,
308+ /// and any periodic flush interleave.
315309 #[ shuttle_test( 2_000 , 3 ) ]
316310 fn concurrent_drops_exit_cleanly_and_flush_once ( ) {
317311 const CLONES : u64 = 2 ;
@@ -350,7 +344,10 @@ mod shuttle_tests {
350344 let mut values = merged. lock ( ) . unwrap ( ) . clone ( ) ;
351345 values. sort ( ) ;
352346 assert_eq ! ( values, ( 0 ..CLONES ) . collect:: <Vec <_>>( ) ) ;
353- assert_eq ! ( flushes. load( Ordering :: SeqCst ) , 1 ) ;
347+ assert ! (
348+ flushes. load( Ordering :: SeqCst ) >= 1 ,
349+ "must flush at least once (at shutdown, possibly earlier too via a periodic flush)"
350+ ) ;
354351 }
355352
356353 /// The shared mpsc channel preserves each sender's own order, so this thread's
@@ -397,4 +394,55 @@ mod shuttle_tests {
397394 . join ( )
398395 . expect ( "worker thread panicked" ) ;
399396 }
397+
398+ /// Same property as `flush_resolves_after_own_prior_sends`, but with
399+ /// enough sends per thread to give the randomized `recv_timeout` wrapper
400+ /// above a real chance to fire a periodic flush (a `Timeout` when the
401+ /// channel is briefly empty) mid-run, not just at the final `flush()`.
402+ /// `flush()`'s own-prior-sends guarantee must hold either way.
403+ #[ shuttle_test( 2_000 , 3 ) ]
404+ fn flush_resolves_after_own_prior_sends_even_with_periodic_flushes ( ) {
405+ const PER_THREAD : u64 = 20 ;
406+
407+ let merged: Arc < Mutex < Vec < u64 > > > = Arc :: default ( ) ;
408+ let flushes = Arc :: new ( AtomicUsize :: new ( 0 ) ) ;
409+ let sink = WorkerSink :: < u64 , _ > :: new (
410+ CollectingSink {
411+ merged : merged. clone ( ) ,
412+ flushes : flushes. clone ( ) ,
413+ } ,
414+ flush_interval ( ) ,
415+ ) ;
416+
417+ let other = {
418+ let sink = sink. clone ( ) ;
419+ shuttle:: thread:: spawn ( move || {
420+ for i in 0 ..PER_THREAD {
421+ sink. send ( i) ;
422+ }
423+ } )
424+ } ;
425+
426+ for i in PER_THREAD ..( PER_THREAD * 2 ) {
427+ sink. send ( i) ;
428+ }
429+ block_on ( sink. flush ( ) ) ;
430+
431+ let values = merged. lock ( ) . unwrap ( ) . clone ( ) ;
432+ for i in PER_THREAD ..( PER_THREAD * 2 ) {
433+ assert ! (
434+ values. contains( & i) ,
435+ "flush() resolved without observing entry {i} sent before it on the same \
436+ thread, even though a periodic flush may have fired mid-run"
437+ ) ;
438+ }
439+
440+ other. join ( ) . unwrap ( ) ;
441+ let handle = Arc :: clone ( & sink. _handle ) ;
442+ drop ( sink) ;
443+ Arc :: into_inner ( handle)
444+ . expect ( "sole handle ref after dropping the only WorkerSink" )
445+ . join ( )
446+ . expect ( "worker thread panicked" ) ;
447+ }
400448}
0 commit comments