use std::sync::mpsc::{self, RecvTimeoutError};
use std::sync::Arc;
use std::thread::{self, JoinHandle};
use std::time::Duration;
use parking_lot::Mutex;
pub struct PeriodicFlusher {
shutdown_tx: mpsc::SyncSender<()>,
handle: Mutex<Option<JoinHandle<()>>>,
}
impl PeriodicFlusher {
#[must_use]
pub fn new<F>(interval: Duration, flush_fn: F) -> Self
where
F: Fn() + Send + Sync + 'static,
{
assert!(
!interval.is_zero(),
"PeriodicFlusher::new: interval must be non-zero (a zero interval would busy-loop)"
);
let (shutdown_tx, shutdown_rx) = mpsc::sync_channel::<()>(1);
let flush_arc = Arc::new(flush_fn);
let worker_flush = Arc::clone(&flush_arc);
let handle = thread::Builder::new()
.name("memscope-flusher".to_string())
.spawn(move || {
worker_loop(shutdown_rx, worker_flush, interval);
})
.expect(
"PeriodicFlusher::new: OS failed to spawn worker thread (resource exhaustion?)",
);
Self {
shutdown_tx,
handle: Mutex::new(Some(handle)),
}
}
pub fn stop(&self) {
let handle_opt = self.handle.lock().take();
let Some(handle) = handle_opt else {
return;
};
let _ = self.shutdown_tx.send(());
if let Err(_panic_payload) = handle.join() {
tracing::warn!(
target: "memscope::periodic_flusher",
"periodic flusher worker thread panicked during shutdown; payload suppressed"
);
}
}
}
impl Drop for PeriodicFlusher {
fn drop(&mut self) {
self.stop();
}
}
fn worker_loop<F: Fn() + Send + Sync + 'static>(
shutdown_rx: mpsc::Receiver<()>,
flush: Arc<F>,
interval: Duration,
) {
loop {
match shutdown_rx.recv_timeout(interval) {
Ok(()) => {
flush_with_trap(&flush);
return;
}
Err(RecvTimeoutError::Timeout) => {
flush_with_trap(&flush);
}
Err(RecvTimeoutError::Disconnected) => {
flush_with_trap(&flush);
return;
}
}
}
}
fn flush_with_trap<F: Fn() + Send + Sync + 'static>(flush: &Arc<F>) {
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
flush();
}));
if result.is_err() {
tracing::warn!(
target: "memscope::periodic_flusher",
"flush closure panicked; suppressing to keep worker alive"
);
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Instant;
use parking_lot::Mutex as PlMutex;
#[test]
fn positive_periodic_flush_fires_multiple_times() {
let counter = Arc::new(AtomicUsize::new(0));
let c = Arc::clone(&counter);
let flusher = PeriodicFlusher::new(Duration::from_millis(50), move || {
c.fetch_add(1, Ordering::SeqCst);
});
std::thread::sleep(Duration::from_millis(200));
let count = counter.load(Ordering::SeqCst);
assert!(
count >= 3,
"expected at least 3 flushes over 200ms with 50ms interval, got {} \
(scheduling jitter may add ticks but not remove them below 3)",
count
);
flusher.stop();
}
#[test]
fn positive_stop_triggers_final_flush() {
let counter = Arc::new(AtomicUsize::new(0));
let c = Arc::clone(&counter);
let flusher = PeriodicFlusher::new(Duration::from_millis(50), move || {
c.fetch_add(1, Ordering::SeqCst);
});
std::thread::sleep(Duration::from_millis(150));
let pre_stop = counter.load(Ordering::SeqCst);
assert!(
pre_stop >= 1,
"expected at least one periodic flush before stop, got {}",
pre_stop
);
flusher.stop();
let post_stop = counter.load(Ordering::SeqCst);
assert!(
post_stop > pre_stop,
"stop() must trigger a final flush: post_stop ({}) must be > pre_stop ({})",
post_stop,
pre_stop
);
}
#[test]
fn positive_stop_returns_within_bound() {
let counter = Arc::new(AtomicUsize::new(0));
let c = Arc::clone(&counter);
let interval = Duration::from_millis(50);
let flusher = PeriodicFlusher::new(interval, move || {
c.fetch_add(1, Ordering::SeqCst);
});
std::thread::sleep(Duration::from_millis(100));
let start = Instant::now();
flusher.stop();
let elapsed = start.elapsed();
let bound = interval + Duration::from_secs(1);
assert!(
elapsed <= bound,
"stop() must return within {:?} (interval + 1s), took {:?}",
bound,
elapsed
);
}
#[test]
fn positive_worker_thread_name() {
let seen_name: Arc<PlMutex<Option<String>>> = Arc::new(PlMutex::new(None));
let sn = Arc::clone(&seen_name);
let flusher = PeriodicFlusher::new(Duration::from_millis(50), move || {
let name = std::thread::current().name().map(|s| s.to_string());
if name.is_some() {
*sn.lock() = name;
}
});
std::thread::sleep(Duration::from_millis(150));
flusher.stop();
let name = seen_name.lock().take();
assert_eq!(
name.as_deref(),
Some("memscope-flusher"),
"worker thread must be named 'memscope-flusher' for diagnostics, got {:?}",
name
);
}
#[test]
#[should_panic(expected = "interval must be non-zero")]
fn negative_zero_interval_panics() {
let _flusher = PeriodicFlusher::new(Duration::ZERO, || {});
panic!("PeriodicFlusher::new with Duration::ZERO must panic before reaching this line");
}
#[test]
fn negative_stop_called_twice_is_noop() {
let counter = Arc::new(AtomicUsize::new(0));
let c = Arc::clone(&counter);
let flusher = PeriodicFlusher::new(Duration::from_millis(50), move || {
c.fetch_add(1, Ordering::SeqCst);
});
std::thread::sleep(Duration::from_millis(100));
flusher.stop();
let after_first = counter.load(Ordering::SeqCst);
assert!(
after_first >= 1,
"expected at least one flush after first stop(), got {}",
after_first
);
flusher.stop();
let after_second = counter.load(Ordering::SeqCst);
assert_eq!(
after_second, after_first,
"second stop() must be a no-op: counter must be unchanged \
(after_first={}, after_second={})",
after_first, after_second
);
}
#[test]
fn negative_stop_immediately() {
let counter = Arc::new(AtomicUsize::new(0));
let c = Arc::clone(&counter);
let flusher = PeriodicFlusher::new(Duration::from_millis(50), move || {
c.fetch_add(1, Ordering::SeqCst);
});
flusher.stop();
let count = counter.load(Ordering::SeqCst);
assert!(
count <= 1,
"immediate stop() should fire at most one flush (the final one), got {}",
count
);
}
#[test]
fn negative_drop_triggers_shutdown() {
let counter = Arc::new(AtomicUsize::new(0));
let c = Arc::clone(&counter);
let flusher = PeriodicFlusher::new(Duration::from_millis(20), move || {
c.fetch_add(1, Ordering::SeqCst);
});
std::thread::sleep(Duration::from_millis(80));
let pre_drop = counter.load(Ordering::SeqCst);
assert!(
pre_drop >= 1,
"expected at least one periodic flush before drop, got {}",
pre_drop
);
drop(flusher);
let post_drop = counter.load(Ordering::SeqCst);
assert!(
post_drop > pre_drop,
"drop() must trigger a final flush: post_drop ({}) > pre_drop ({})",
post_drop,
pre_drop
);
}
#[test]
fn negative_flush_panic_does_not_kill_worker() {
let counter = Arc::new(AtomicUsize::new(0));
let c = Arc::clone(&counter);
let flusher = PeriodicFlusher::new(Duration::from_millis(20), move || {
let prev = c.fetch_add(1, Ordering::SeqCst);
if prev == 0 {
panic!("intentional flush panic — worker must survive via catch_unwind");
}
});
std::thread::sleep(Duration::from_millis(100));
let count = counter.load(Ordering::SeqCst);
assert!(
count >= 2,
"worker must survive a flush panic and continue flushing: \
expected >= 2 (1 panicked + 1 survived), got {}",
count
);
flusher.stop();
let final_count = counter.load(Ordering::SeqCst);
assert!(
final_count > count,
"final flush must fire even after a prior panic: \
final_count ({}) > count ({})",
final_count,
count
);
}
#[test]
fn stress_50_concurrent_stop_calls() {
let counter = Arc::new(AtomicUsize::new(0));
let c = Arc::clone(&counter);
let flusher = Arc::new(PeriodicFlusher::new(Duration::from_millis(10), move || {
c.fetch_add(1, Ordering::SeqCst);
}));
std::thread::sleep(Duration::from_millis(50));
let pre_stop = counter.load(Ordering::SeqCst);
assert!(
pre_stop >= 1,
"expected at least one periodic flush before the stop storm, got {}",
pre_stop
);
let mut handles = Vec::with_capacity(50);
for _ in 0..50 {
let f = Arc::clone(&flusher);
handles.push(std::thread::spawn(move || f.stop()));
}
for h in handles {
h.join()
.expect("every stop() thread must join without panic (no deadlock)");
}
drop(flusher);
let post_stop = counter.load(Ordering::SeqCst);
assert!(
post_stop > pre_stop,
"final flush must fire during stop storm: post_stop ({}) > pre_stop ({})",
post_stop,
pre_stop
);
let storm_increments = post_stop - pre_stop;
assert!(
storm_increments <= 3,
"at most a few flushes (one periodic + one final) should fire during \
the stop storm, got {} increments; 50 concurrent stop() calls must \
not cause 50 final flushes",
storm_increments
);
}
}
#[cfg(loom)]
mod loom_tests {
use loom::sync::atomic::{AtomicUsize, Ordering};
use loom::sync::Arc;
use loom::sync::Mutex;
use loom::thread;
#[test]
fn concurrent_stop_joins_exactly_once() {
loom::model(|| {
let handle = Arc::new(Mutex::new(Some(())));
let join_count = Arc::new(AtomicUsize::new(0));
let h1_handle = Arc::clone(&handle);
let h1_count = Arc::clone(&join_count);
let h2_handle = Arc::clone(&handle);
let h2_count = Arc::clone(&join_count);
let t1 = thread::spawn(move || {
if h1_handle.lock().take().is_some() {
h1_count.fetch_add(1, Ordering::SeqCst);
}
});
let t2 = thread::spawn(move || {
if h2_handle.lock().take().is_some() {
h2_count.fetch_add(1, Ordering::SeqCst);
}
});
t1.join().expect("loom model: t1 must join without panic");
t2.join().expect("loom model: t2 must join without panic");
assert_eq!(
join_count.load(Ordering::SeqCst),
1,
"exactly one stop() call must win the join under any interleaving; \
got {}",
join_count.load(Ordering::SeqCst)
);
});
}
#[test]
fn concurrent_stop_idempotent_under_three_threads() {
loom::model(|| {
let handle = Arc::new(Mutex::new(Some(())));
let join_count = Arc::new(AtomicUsize::new(0));
let threads: Vec<_> = (0..3)
.map(|_| {
let h = Arc::clone(&handle);
let c = Arc::clone(&join_count);
thread::spawn(move || {
if h.lock().take().is_some() {
c.fetch_add(1, Ordering::SeqCst);
}
})
})
.collect();
for (i, t) in threads.into_iter().enumerate() {
t.join()
.unwrap_or_else(|_| panic!("loom model: thread {} must join without panic", i));
}
assert_eq!(
join_count.load(Ordering::SeqCst),
1,
"exactly one of three concurrent stop() calls must win the join; \
got {}",
join_count.load(Ordering::SeqCst)
);
});
}
}