tl;dr check out my WIP crate async-dropper with a couple implementations for AsyncDrop-like functionality. One is stolen borrowed from StackOverflow (thanks @paholg!) but the other approach (derive-based) might actually be interesting.
Something about not having an AsyncDrop yet (if ever) makes me uneasy and makes me want to hack up a solution to it!
In practice, tokio and other crates provide good-enough protections, and there are options like using packages that provide defer macros (if you like that kind of thing), but I really like the idea of being able to impl AsyncDrop for T.
Like most people I've come across this StackOverflow post which suggests some good solutions, and I wanted to collect them in a crate I can re-use.
The idea behind this crate is to steal surface the approach created by @paholg, and try to add something new -- I want to be able to derive AsyncDrop from anything that is Default + PartialEq + Eq.
The idea I'm going for is that if the thing you're dealing with is equal to the default version, it doesn't need to be async-dropped (essentially, block_on'd).
Check out what I have so far in the repo, would love some feedback!
Right now the wrapper approach works fine, but the new approach has some issues:
the tokio version never exits (probably bad use of block_on)
the async-std infinite loops (???)
Very divergent problems but I'm not too worried about them, I assume I just have something subtly wrong with my implementation & use of those async libs rather than the whole approach being impossible (those should be solved by this weekend!). Would love to hear any opinions.
// First, the preamble below which is shared across implementations (tokio/async-std)
#[derive(Debug)]
pub enum AsyncDropError {
UnexpectedError(Box<dyn std::error::Error>),
Timeout,
}
/// What to do when a drop fails
#[derive(Debug, PartialEq, Eq)]
pub enum DropFailAction {
// Ignore the failed drop
Continue,
// Elevate the drop failure to a full on panic
Panic,
}
#[async_trait]
trait AsyncDrop: Default + PartialEq + Eq {
/// Operative drop that does async operations, returning
async fn async_drop(&mut self) -> Result<(), AsyncDropError> {
Ok(())
}
/// Timeout for drop operation, meant to be overriden if needed
fn drop_timeout(&self) -> Duration {
Duration::from_secs(3)
}
/// What to do what a drop fails
fn drop_fail_action(&self) -> DropFailAction {
DropFailAction::Continue
}
}
/// Utility function unique to #ident which retrieves a shared mutable single default instance of it
/// that single default instance is compared to other instances and indicates whether async drop
/// should be called
#[allow(non_snake_case)]
fn #shared_default_name() -> &'static std::sync::Mutex<#ident> {
#[allow(non_upper_case_globals)]
static #shared_default_name: std::sync::OnceLock<std::sync::Mutex<#ident>> = std::sync::OnceLock::new();
#shared_default_name.get_or_init(|| std::sync::Mutex::new(#ident::default()))
}
// Then the async platform specific specific code:
#[automatically_derived]
#[async_trait]
impl Drop for #ident {
fn drop(&mut self) {
// We consider a self that is completley equivalent to it's default version to be dropped
let thing = #shared_default_name();
if *thing.lock().unwrap() == *self {
return;
}
// Ensure that the default_version is manually dropped
let mut original = Self::default();
std::mem::swap(&mut original, self);
// Spawn a task to do the drop
let task = ::tokio::spawn(async move {
let drop_fail_action = original.drop_fail_action();
match ::tokio::time::timeout(
original.drop_timeout(),
original.async_drop(),
).await {
Err(e) => {
match drop_fail_action {
DropFailAction::Continue => {}
DropFailAction::Panic => {
panic!("async drop failed: {e}");
}
}
},
Ok(_) => {},
}
});
// Perform a synchronous wait
::futures::executor::block_on(task).unwrap();
}
}
The hardest piece so far was trying to figure out how to store a T::default() somewhere that I ran re-use for checks (essentially, so I can choose whether to wait or not), without creating more garbage.
Thanks for the suggestion -- I changed that and fixed one more bug (I needed one more method, reset(&mut self) to make things work) -- the tokio version is working!
Going around and changing other spots where block_in_place was not used.
[EDIT] - Do want to note that the version without block_in_place did work for the -simple case -- replaced it anyway though!