Description
Hi. I'm implementing a library at a low level which allocates and frees memory manually. To try to keep it correct I run example binaries and tests under Valgrind a lot. However, when using async-std
my programs and tests very often seem to not correctly join threads before exiting, resulting in leaks that make Valgrind sad and my automated tests fail. I experience this on Linux and have not tried it elsewhere.
A simple failing test. Placed under tests/all_tests.rs
:
#[async_std::test]
async fn just_sleep() {
async_std::task::sleep(std::time::Duration::from_millis(1)).await;
}
You can run it under vanilla Valgrind with valgrind --leak-check=full ./target/debug/whatever_the_filename_is
. But cargo valgrind
has prettier output. Due to the racy nature of this it might not always fail, but for me it does ~90% of the runs:
$ cargo valgrind --test all_tests
....
Error Leaked 24 B
Info at malloc (vg_replace_malloc.c:309)
at alloc::alloc::alloc (alloc.rs:84)
at alloc::alloc::exchange_malloc (alloc.rs:206)
at alloc::sync::Arc<T>::new (sync.rs:302)
at futures_timer::global::current_thread_waker (global.rs:104)
at futures_timer::global::run (global.rs:59)
at futures_timer::global::HelperThread::new::{{closure}} (global.rs:28)
at std::sys_common::backtrace::__rust_begin_short_backtrace (backtrace.rs:126)
at std::thread::Builder::spawn_unchecked::{{closure}}::{{closure}} (mod.rs:470)
at <std::panic::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once (panic.rs:315)
at std::panicking::try::do_call (panicking.rs:292)
at __rust_maybe_catch_panic (lib.rs:80)
Error Leaked 288 B
Info at calloc (vg_replace_malloc.c:762)
at allocate_dtv
at _dl_allocate_tls
at pthread_create@@GLIBC_2.2.5
at std::sys::unix::thread::Thread::new (thread.rs:67)
at std::thread::Builder::spawn_unchecked (mod.rs:489)
at std::thread::Builder::spawn (mod.rs:382)
at futures_timer::global::HelperThread::new (global.rs:26)
at <futures_timer::timer::TimerHandle as core::default::Default>::default (timer.rs:281)
at futures_timer::delay::Delay::new (delay.rs:39)
at async_std::io::timeout::timeout::{{closure}} (timeout.rs:40)
at <std::future::GenFuture<T> as core::future::future::Future>::poll::{{closure}} (future.rs:43)
Summary Leaked 312 B total
A very similar leak stack trace can be obtained from the following test:
#[test]
fn just_sync_sleep() {
std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_millis(1));
});
}
Which is fixed by joining all threads before exiting:
#[test]
fn just_sync_sleep() {
let t = std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_millis(1));
});
t.join()
}
This leads me to suspect the #[async_std::main]
and #[async_std::test]
macros don't properly join all threads before exiting.