What is the idiomatic way of cleaning up async resources in rust?

So I have a resource that I can get that needs to be locked to one user at a time. It is accessed by an HTTP call. The library we are using for making the HTTP call makes use of asynchronous calls.
I would like to free up the resource on destruction of the class, if it hasn't been freed up already in the positive test case flow.
The problem is, you can't call asynchronous functions from the drop() function.

I found this:

https://docs.rs/async-drop/0.0.0/async_drop/

and this article:

https://boats.gitlab.io/blog/post/poll-drop/

So the answer basically appears to be "no."
But that doesn't solve the problem of what is the correct/idiomatic way to solve the problem?

Is there a known common solution to this? All I could find was people asking questions.

Here's my Cargo.toml

[package]
name = "asyncdrop"
version = "0.1.0"
authors = ["rustuser <pirera3226@mailetk.com>"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
tokio = { version = "0.2", features = ["full"] }
anyhow = "1.0"

and the source file example of the problem.

use std::sync::Arc;
use tokio::sync::RwLock;

struct Operation {
    handle: Arc<RwLock<Option<u32>>>,
}

impl Operation {
    fn new() -> Operation {
        Operation {
            handle: Arc::new(RwLock::new(Some(1))),
        }
    }

    async fn lock_resource(&self) -> Result<(), anyhow::Error> {
        // pretend we're making an http call here
        let mut locked = self.handle.write().await; // I use tokio here to force the issue that I need to call await.
        *locked = Some(1);
        return Ok(());
    }

    async fn unlock_resource(&self) -> Result<(), anyhow::Error> {
        // pretend we're making another http call here
        let mut locked = self.handle.write().await;
        if *locked == None {
            return Ok(());
        }
        *locked = None;
        return Ok(());
    }

    async fn go(&self) -> Result<(), anyhow::Error> {
        self.lock_resource().await?;

        println!("Uh oh, I exhibit an error and return early.");
        if true {
            return Err(anyhow::anyhow!("failure"));
        }
        self.unlock_resource().await?;
        return Ok(());
    }
}

impl Drop for Operation {
    fn drop(&mut self) {
        println!("Operation going out of scope, about to free_data()");
        self.unlock_resource().await;
    }
}

#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
    println!("Hello, world!");
    let example = Operation::new();
    example.go().await?;

    return Ok(()); // Drop should free the resource here.
}

To be honest, the idiomatic way is to design your code such that you don't need async drop.

If you have another task somewhere, you could send it a message, but you still have to behave sanely if that other task is killed.

3 Likes

Yup, asynchronous dropping is a tough problem:

yeah, so I had it set up so that any call to the thing that allocates resources would be wrapped in a function, so that any way the callee could possibly fail and exit early the calling function would still be able to call free_resource() on it on the way out.
But there are some weird failure modes where the thing ends up in a state where you can't know if it needs to be freed unless you know about the innards of the class. This is why I figured drop was the way to go.

I realize "design it differently" is a catch all for "it doesn't work" but I was just looking for some tips or other rust-y ways of trying to take care of the problem.
But if there isn't a way, there isn't a way.

Thanks.