I'm working on a small ad-hoc AsyncDrop crate

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.

How does it work? Could you post what your macro expands to?

Sure -- it's pretty simple:

// 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.

If you're going to block the runtime, then you should call tokio::task::block_in_place.

1 Like

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!

Looks like the implementation is working now! :tada:

Still not sure if it's a good idea, but I guess time will tell.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.