Skip to content

Commit 977d61c

Browse files
author
tiny-ac
committed
test(metrique-aggregation): make periodic flush reachable under shuttle
1 parent 6095fb4 commit 977d61c

3 files changed

Lines changed: 114 additions & 26 deletions

File tree

metrique-aggregation/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ __build_examples_for_rustdoc = ["metrique/emf", "metrique/test-util"]
4949
# Runs WorkerSink on shuttle-native primitives instead of std, so Shuttle's
5050
# scheduler can explore thread interleavings (e.g. concurrent sends racing the
5151
# last sender being dropped). Test-only.
52-
_shuttle = ["dep:shuttle"]
52+
_shuttle = ["dep:shuttle", "metrique-writer-core/_shuttle"]
5353

5454
# We need to name one example so that our examples that use dev-dependencies get scraped
5555
[[example]]

metrique-aggregation/src/sink/worker.rs

Lines changed: 67 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -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")))]
2928
use 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"))]
227222
mod 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
}

metrique-writer-core/src/shuttle_test_support.rs

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,12 +63,7 @@ impl<T> ArrayQueue<T> {
6363

6464
/// Whether `now` has reached `deadline`. Real wall-clock time barely
6565
/// advances during a fast shuttle iteration, so `now >= deadline` alone
66-
/// would (almost) never fire -- OR in a random chance, so the scheduler
67-
/// explores both outcomes. Same trick as `dial9-core`'s `recv_timeout`
68-
/// wrapper (a sibling project), adapted to wrap a comparison instead of a
69-
/// blocking call. The non-shuttle equivalent (a trivial `now >= deadline`)
70-
/// stays local to each crate, since it's real production logic, not test
71-
/// support.
66+
/// would (almost) never fire.
7267
#[doc(hidden)]
7368
pub fn deadline_reached(now: std::time::Instant, deadline: std::time::Instant) -> bool {
7469
use shuttle::rand::Rng;
@@ -263,3 +258,48 @@ impl<T> Drop for GuardWeak<T> {
263258
self.0.lock().unwrap().weak -= 1;
264259
}
265260
}
261+
262+
#[doc(hidden)]
263+
pub use shuttle::sync::mpsc::Sender;
264+
#[doc(hidden)]
265+
pub use shuttle::thread;
266+
267+
/// Shuttle-visible substitute for `std::sync::mpsc::channel()`, wrapping the
268+
/// receiver half so `recv_timeout` can actually return `Timeout` -- shuttle's
269+
/// own `recv_timeout` never times out, a known gap documented in Shuttle
270+
/// itself:
271+
/// https://github.com/awslabs/shuttle/blob/c8a46d3965048df3207ec920dae066bc9c4d9d89/shuttle-std/src/sync/mpsc.rs#L433.
272+
/// Mostly checks non-blockingly, occasionally really blocks instead.
273+
#[doc(hidden)]
274+
pub fn channel<T>() -> (Sender<T>, RecvTimeoutReceiver<T>) {
275+
let (tx, rx) = shuttle::sync::mpsc::channel();
276+
(tx, RecvTimeoutReceiver { inner: rx })
277+
}
278+
279+
#[doc(hidden)]
280+
pub struct RecvTimeoutReceiver<T> {
281+
inner: shuttle::sync::mpsc::Receiver<T>,
282+
}
283+
284+
impl<T> RecvTimeoutReceiver<T> {
285+
#[doc(hidden)]
286+
pub fn recv_timeout(
287+
&self,
288+
_timeout: std::time::Duration,
289+
) -> Result<T, std::sync::mpsc::RecvTimeoutError> {
290+
use shuttle::rand::Rng;
291+
use std::sync::mpsc::{RecvTimeoutError, TryRecvError};
292+
293+
if shuttle::rand::thread_rng().gen_bool(0.8) {
294+
match self.inner.try_recv() {
295+
Ok(val) => Ok(val),
296+
Err(TryRecvError::Empty) => Err(RecvTimeoutError::Timeout),
297+
Err(TryRecvError::Disconnected) => Err(RecvTimeoutError::Disconnected),
298+
}
299+
} else {
300+
self.inner
301+
.recv()
302+
.map_err(|_| RecvTimeoutError::Disconnected)
303+
}
304+
}
305+
}

0 commit comments

Comments
 (0)