How to access a global resource in an asynchronous context?

777 views Asked by At

I've been writing a small web service with the tide framework in the async-std flavor. The project uses a database that is used sparsely but throughout the entire project.

I thought it would be reasonable to declare it as a global variable, so that I wouldn't have to include in almost every function interface, just for it to be passed along unused. After a bit of arguing with the compiler about the number of Rust design principles that I was breaking, I settled on the following declaration:

pub static DATABASE: RwLock<Database> = const_rwlock(Database::new());

const_rwlock() is a constant Rwlock constructor from the parking_lot crate (the same as the RwLock on nightly as far as I know) and Database is my own type that gets later filled with the connection details and handles the actual communication and initialization of the database in asynchronous manner.

Everything looked alright, as I could use it like so:

let data = DATABASE.read().get_data(&key).await;

even with async methods in async functions.

The problem arises when I try to pass this functionality into the web framework. I'm supposed to do so by passing in an async block closure that has to be Send, but when I write something like this:

// the actual function to start the servers looks essentially like this
start_server(|| async move {
    // this is run asynchronously for every connection by the framework
    let token = do_stuff();

    let result = DATABASE.read().check(token).await;
});

I get an error, because the entire future is no longer Send. It makes sense to me, because I'm locking a mutex with the .read() call, and then packing the entire thing into a future and awaiting it. That means that the locked mutex might be sent to another thread, which doesn't seem sensible.

This leads me to think that I designed the entire global variable wrong and I have no better ideas.

What would be a good way to represent a resource that is used in many places in async code? I'd like to avoid the "pass it everywhere" route.

This is a somewhat minimal code example. I extracted the relevant code from the tide crate to try and reduce the scope of this question.

use async_std::sync::Arc;
use parking_lot::{const_rwlock, RwLock};
use std::future::Future;

pub static DATABASE: RwLock<Database> = const_rwlock(Database::new());
// const mutex can only be constructed with new() on nightly
//pub static DATABASE: RwLock<Database> = RwLock::new(Database::new());

pub struct Database {
    // Here would be the actual connection stored
    pool: Option<()>,
}

impl Database {
    const fn new() -> Self {
        Database {
            pool: None,
        }
    }

    pub async fn init(&mut self) {
        self.pool = Some(());
    }

    pub async fn check(&self, token: &str) -> bool {
        if token == "vibe" {
            return true;
        } else {
            return false;
        }
    }
}

#[async_std::main]
async fn main() {
    DATABASE.read().init().await;

    Server::new(|| async move {
        // XXX There's the problem
        let result = DATABASE.read().check("vibe").await;

        if result {
            Ok(())
        } else {
            Err(())
        }
    });
}

// Everything below is part of some library and can't be changed by me

struct Server<H> {
    handler: Arc<H>
}

impl<H, Fut> Server<H>
where
    // Those requirements are verbatim copied from the framework code
    H: Fn() -> Fut + Sync + Send + 'static,
    Fut: Future<Output = Result<(), ()>> + Send + 'static,
{
    pub fn new(handler: H) -> Self {
        Self {
            handler: Arc::new(handler),
        }
    }
}

Cargo.toml:

[package]
name = "server"
version = "0.1.0"
edition = "2018"

[dependencies]
parking_lot = "0.11"
async-std = { version = "1.6", features = ["attributes"] }

After running cargo check on it, I get the described error:

    Checking server v0.1.0 (/XXX/server)
error: future cannot be sent between threads safely
  --> src/main.rs:36:5
   |
36 |     Server::new(|| async move {
   |     ^^^^^^^^^^^ future created by async block is not `Send`
...
60 |     pub fn new(handler: H) -> Self {
   |     ------------------------------ required by `Server::<H>::new`
   |
   = help: within `impl Future`, the trait `std::marker::Send` is not implemented for `*mut ()`
note: future is not `Send` as this value is used across an await
  --> src/main.rs:38:22
   |
38 |         let result = DATABASE.read().check("vibe").await;
   |                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ first, await occurs here, with `DATABASE.read()` maybe used later...
note: `DATABASE.read()` is later dropped here
  --> src/main.rs:38:57
   |
38 |         let result = DATABASE.read().check("vibe").await;
   |                      ---------------                    ^
   |                      |
   |                      has type `parking_lot::lock_api::RwLockReadGuard<'_, parking_lot::RawRwLock, Database>` which is not `Send`
help: consider moving this into a `let` binding to create a shorter lived borrow
  --> src/main.rs:38:22
   |
38 |         let result = DATABASE.read().check("vibe").await;
   |                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

I believe I interpreted this error correctly at a high level (can't move lock guard between threads) and it is explained on low level (*mut () is not Send) in this error message.

0

There are 0 answers