3
votes

I am new to Rust. I am using mongodb with async runtime (tokio).

I want to initialize mongo client globally, So I used a crate called lazy_static.The problem is that mongodb connect asynchronously, and right now lazy_static don't support async keyword...

use mongodb::Client;
async {
   let client = Client::with_uri_str(&env_var("MONGO_URL")).await.unwrap();
}

So how do I initialize client object globally ?

Related:

Async/await support?
alternative to using 'await' with lazy_static! macro in rust?

1
If blocking on initialization is ok using whatever block_on function tokio provides instead of awaiting the result will get the job done.Aiden4
Your linked StackOverflow Q&A has the two suggestions I would have: either block_on or use a OnceCell. Why are the answers there insufficient?kmdreko
As I understand it, what you're looking for is sort of a OnceCell wrapper that works like lazy_static!, except it is initialized with an async block instead of a closure. However, in this case, it seems like the mongodb library is poorly coded and the with_uri_str method doesn't really need to be async, so I would just use block_on.Coder-256
Thanks to all for your time, but using your approach create new issues, I am still looking for answer, with minimum Minimal, Reproducible ExampleMohammed
@Coder-256, @Aiden4 , @kmdreko, I tried OnceCell , futures::executor::block_on , tokio::runtime::Runtime::new etc, they all create new problems, like “Cannot start a runtime from within a runtime” or "thread 'tokio-runtime-worker' panicked at 'cannot execute LocalPool executor from within another executor: EnterError'" and much more...Mohammed

1 Answers

4
votes

If you use a new runtime and the lazy static is first used within the context of an existing runtime, like in this example:

use lazy_static::lazy_static;
use mongodb::Client;

lazy_static! {
    static ref CLIENT: Client = {
        tokio::runtime::Runtime::new().unwrap().block_on(async {
            let uri = std::env::var("MONGO_URL").unwrap();
            let client = Client::with_uri_str(&uri).await.unwrap();

            client
        })
    };
}

#[tokio::main]
async fn main() {
    let _db = CLIENT.database("local");
}

You'll get the error mentioned:

thread 'main' panicked at 'Cannot start a runtime from within a runtime. This happens because a function (like `block_on`) attempted to block the current thread while the thread is being used to drive asynchronous tasks.', C:\Users\kmdreko\.cargo\registry\src\github.com-1ecc6299db9ec823\tokio-1.6.1\src\runtime\enter.rs:39:9

You may be able to circumvent this by using a different runtime (futures vs tokio vs async-std) but that's not ideal since it'd still be blocking the underlying one.


A relatively straight-forward way to solve this is not try to do it lazily and initialize it immediately in main. That way you can utilize an asynchronous runtime directly and not worry about needing one elsewhere:

use mongodb::Client;
use once_cell::sync::OnceCell;

static CLIENT: OnceCell<Client> = OnceCell::new();

#[tokio::main]
async fn main() {
    let uri = std::env::var("MONGO_URL").unwrap();
    let client = Client::with_uri_str(&uri).await.unwrap();
    CLIENT.set(client).unwrap();

    let _db = CLIENT.get().unwrap().database("local");
}

This can be done via OnceCell (as seen above) or something like a RwLock if necessary.


The most direct answer to what you're trying to achieve would be to use the async_once crate, which makes it use the receiver's runtime to drive the asynchronous function.

use async_once::AsyncOnce;
use lazy_static::lazy_static;
use mongodb::Client;

lazy_static! {
    static ref CLIENT: AsyncOnce<Client> = AsyncOnce::new(async {
        let uri = std::env::var("MONGO_URL").unwrap();
        let client = Client::with_uri_str(&uri).await.unwrap();

        client
    });
}

#[tokio::main]
async fn main() {
    let _db = CLIENT.get().await.database("local");
}

This assumes all or nearly all uses of the client will be in asynchronous contexts.