#macro-derive #async #async-trait #tokio #drop #async-std #async-await

async-dropper

The least-worst ad-hoc implementation of AsyncDrop as a Derive macro

13 unstable releases (3 breaking)

0.3.1 Oct 26, 2023
0.3.0 Oct 20, 2023
0.2.5 Oct 18, 2023
0.2.3 Aug 9, 2023
0.0.1 Jul 29, 2023

#159 in Asynchronous

Download history 960/week @ 2024-08-05 1227/week @ 2024-08-12 1134/week @ 2024-08-19 1358/week @ 2024-08-26 1233/week @ 2024-09-02 910/week @ 2024-09-09 600/week @ 2024-09-16 570/week @ 2024-09-23 542/week @ 2024-09-30 853/week @ 2024-10-07 590/week @ 2024-10-14 831/week @ 2024-10-21 792/week @ 2024-10-28 634/week @ 2024-11-04 643/week @ 2024-11-11 788/week @ 2024-11-18

2,902 downloads per month
Used in 7 crates (3 directly)

MIT license

50KB
646 lines

🗑 async-dropper

async-dropper is probably the least-worst ad-hoc AsyncDrop implementation you've seen, and it works in two ways:

  • async_dropper::AsyncDropper is stolen nearly verbatim from this StackOverflow answer (thanks to paholg!)
  • async_dropper::AsyncDrop is a Trait and custom derive macro, which try to use Default and PartialEq to determine when to async drop, automatically while Droping.

The code in this crate was most directly inspired by this StackOverflow thread on Async Drop and many other conversations:

Install

You must set features on this crate, as it works with both async runtimes, and can use either approach outlined above:

# if using tokio, choose one of the two lines below
cargo add async-dropper --features tokio,derive     # use tokio, with the derive approach
cargo add async-dropper --features tokio,simple     # use tokio, with the simple approach

# if using async-std, chose one of the two lines below
cargo add async-dropper --features async-std,derive # use async-std, with the derive approach
cargo add async-dropper --features async-std,simple # use async-std, with the simple approach

If you're editing Cargo.toml by hand, choose one of the following lines:

[dependencies]
#async-dropper = { version = "0.3", features = [ "tokio", "derive" ] }
#async-dropper = { version = "0.3", features = [ "tokio", "simple" ] }

#async-dropper = { version = "0.3", features = [ "async-std", "derive" ] }
#async-dropper = { version = "0.3", features = [ "async-std", "simple" ] }

Warning async-dropper does not allow using both async-std and tokio features at the same time (see the FAQ below). You can, however, use both the simple and derive features at the same time

Quickstart

async_dropper::simple

To use the "simple" version which uses a wrapper struct (AsyncDropper<T>), see examples/simple_tokio.rs:

use std::{
    result::Result,
    time::Duration,
};

use async_dropper_simple::{AsyncDrop, AsyncDropper};
use async_trait::async_trait;

// NOTE: this example is rooted in crates/async-dropper

/// This object will be async-dropped (which must be wrapped in AsyncDropper)
#[derive(Default)]
struct AsyncThing(String);

#[async_trait]
impl AsyncDrop for AsyncThing {
    async fn async_drop(&mut self) {
        eprintln!("async dropping [{}]!", self.0);
        tokio::time::sleep(Duration::from_secs(2)).await;
        eprintln!("dropped [{}]!", self.0);
    }
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    {
        let _example_obj = AsyncDropper::new(AsyncThing(String::from("test")));
        eprintln!("here comes the (async) drop");
        // drop will be triggered here, and it will take *however long it takes*
        // you could also call `drop(_example_obj)`
    }

    Ok(())
}

You can run the example and see the output:

cargo run --example simple-tokio --features=tokio

async_dropper::derive

The derive macro is a novel (and possibly foolhardy) attempt to implement AsyncDrop without actually wrapping the existing struct.

async_dropper::derive uses Default and PartialEq to check if the struct in question is equivalent to it's default.

For this approach to work well your T should have cheap-to-create Defaults, and comparing a default value to an existing value should meaningfully differ (and identify an object that is no longer in use). Please think thoroughly about whether this model works for your use case.

For an example, see examples/derive_tokio.rs:

use std::{
    result::Result,
    time::Duration,
};

use async_dropper::derive::AsyncDrop;
use async_trait::async_trait;

/// This object will be async-dropped
///
/// Objects that are dropped *must* implement [Default] and [PartialEq]
/// (so make members optional, hide them behind Rc/Arc as necessary)
#[derive(Debug, Default, PartialEq, Eq, AsyncDrop)]
struct AsyncThing(String);

/// Implementation of [AsyncDrop] that specifies the actual behavior
#[async_trait]
impl AsyncDrop for AsyncThing {
    // simulated work during async_drop
    async fn async_drop(&mut self) -> Result<(), AsyncDropError> {
        eprintln!("async dropping [{}]!", self.0);
        tokio::time::sleep(Duration::from_secs(2)).await;
        eprintln!("dropped [{}]!", self.0);
        Ok(())
    }

    fn drop_timeout(&self) -> Duration {
        Duration::from_secs(5) // extended from default 3 seconds, as an example
    }

    // NOTE: the method below is automatically derived for you, but you can override it
    // make sure that the object is equal to T::default() by the end, otherwise it will panic!
    // fn reset(&mut self) {
    //     self.0 = String::default();
    // }

    // NOTE: below was not implemented since we want the default of DropFailAction::Continue
    // fn drop_fail_action(&self) -> DropFailAction;
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    {
        let _example_obj = AsyncThing(String::from("test"));
        eprintln!("here comes the (async) drop");
        // drop will be triggered here
        // you could also call `drop(_example_obj)`
    }

    Ok(())
}

You can run the example and see the output:

cargo run --example derive-tokio --features=tokio

Supported environments

async-dropper works with the following async environments:

Name Supported?
Async w/ tokio
Async w/ async-std

FAQ

Why does async-dropper assume that I'm using either async-std or tokio?

Because you probably are. If this is a problem for you, it can be changed, please file an issue.

Why do I have to choose between simple and derive features?

The simple strategy and derive strategy impose different requirements on the T on which they act.

To avoid requiring unnnecessary and possibly incompatible traits, you should choose one of the features (i.e. approaches) to go with.

If this "feature" presents an issue for you, it can be changed, please file an issue.

What does async_dropper::derive cost?

There is waste introduced by async_dropper::derive, namely:

  • One Mutex-protected T::default() instance of your type, that exists as long as the program runs
  • One extra T::default() that is made of an individual T being dropped.

As a result, every drop you perform on a T will perform two drops -- one on a T::default() and another on your T, which has been converted to a T::default (via reset(&mut self)).

Development

To get started working on developing async-dropper, run the following just targets:

just setup build

To check that your changes are fine, you'll probably want to run:

just test

If you want to see the full list of targets available that you can run just without any arguments.

just

There are a few useful targets like just build-watch which will continuously build the project thanks to cargo watch.

Releasing

From the top level of this repository:

PUBLISH_CRATE=yes PKG=<crate name> just release <version>

For example, to create the next semver patch release for async-dropper-simple:

PUBLISH_CRATE=yes PKG=async-dropper-simple just release patch

Contributing

Contributions are welcome! If you find a bug or an impovement that should be included in async-dropper, [create an issue][crate-issue] or open a pull request.

Dependencies

~0.9–12MB
~137K SLoC