0

I'm writing a discord bot in Rust with Serenity. In order to populate some embed headers, I need bot-related info (user & owner, hereafter named BOT) and I'm trying to use the Lazy struct from the once_cell crate to make that happen.

The catch is that this BOT data can only be queried using async functions with serenity's Http module and I'm struggling to make that work.

I set up a few slash-commands that each have their own handler function. When I dereference BOT (this is what's supposed to evaluate it (only once), right ?) from one of those handler functions, my Lazy closure blocks on the first .await it encounters.

On the other hand, it just works when I dereference BOT from main (tokio async).

The following code therefore prints "0,1,2,3" when dereferencing BOT from main but only "0,1" when doing so from a specific slash-command handler (BoxedFuture) :

use once_cell::sync::Lazy;
use serenity::{http::Http, model::user::User};

pub struct Bot {
    pub user: User,
    pub owner: User,
}

pub static BOT: Lazy<Bot> = Lazy::new(|| {
    println!("0");
    let http_client = Http::new(env!("DISCORD_BOT_TOKEN"));

    futures::executor::block_on(async {
        println!("1");
        let user = http_client
            .get_current_user()
            .await
            .expect("An HTTP client error occured")
            .into();

        println!("2");
        let owner = http_client
            .get_current_application_info()
            .await
            .expect("An HTTP client error occured")
            .owner;

        println!("3");
        Bot { user, owner }
    })
});

Can anyone explain why that is ?

Fointard
  • 43
  • 1
  • 7
  • 3
    You can't use `block_on` inside of an async task. It would block the async loop, causing a deadlock. You can **never** call anything blocking inside of an async task. This is true even though your `Lazy` closure is not an async closure, if you are accessing `BOT` from withing async tasks. – Finomnis Aug 17 '22 at 18:21
  • Does this answer your question? [Rust lazy\_static with async/await?](https://stackoverflow.com/questions/67650879/rust-lazy-static-with-async-await) – Finomnis Aug 17 '22 at 18:23
  • I don't think that the link you provided answers my question. I've stumbled upon it already but didn't manage to get sth working. I'll give it another look though. What I need now given you comment is a way to replace that `block_on` call. – Fointard Aug 17 '22 at 18:36
  • 2
    No, what you are trying to do is just simply not possible without a non-async once. The answer in the duplicate lays it out exactly as you need it. You will never be able to execute async code from within a sync function that is called from another async. The calling order async -> sync -> async is an impossible situation, because there is no way to propagate an `await` through a sync function without blocking the executor. Your sync needs to become async, like shown with `AsyncOnce` in the duplicate. – Finomnis Aug 17 '22 at 18:38
  • 2
    The last example is almost 1:1 your situation, where a server connection needs to be instantiated globally and lazily. – Finomnis Aug 17 '22 at 18:45
  • Do you think the [`async-once-cell`](https://crates.io/crates/async-once-cell) crate could do what I need ? I'm trying to instantiate a `Lazy` with the `new()` method but the compiler isn't happy about it and I can't figure out what's wrong... – Fointard Aug 17 '22 at 19:13
  • Probably, I'm not familiar with that crate. There is usually many ways to solve a problem. – Finomnis Aug 17 '22 at 19:19
  • 1
    If you make sure to dereference it (or call `force()`) at the beginning of `main()`, it _should_ be fine, but I would not rely on that. If you do it before you construct the runtime it should be fine. But you need to consider whether you are ready to pay the price of always initializing it, even when unnecessary. – Chayim Friedman Aug 18 '22 at 03:28
  • @ChayimFriedman I think your answer here would make a worthy addition to the linked Q&A if you'd like to duplicate it over there. – kmdreko Sep 19 '22 at 03:52

1 Answers1

5

You should never block inside an async runtime. Never, ever.

You can use tokio::sync::OnceCell instead of once_cell:

pub async fn get_bot() -> &'static Bot {
    static BOT: tokio::sync::OnceCell<Bot> = tokio::sync::OnceCell::const_new();
    BOT.get_or_init(|| async {
        println!("0");
        let http_client = Http::new(env!("DISCORD_BOT_TOKEN"));

        println!("1");
        let user = http_client
            .get_current_user()
            .await
            .expect("An HTTP client error occured")
            .into();

        println!("2");
        let owner = http_client
            .get_current_application_info()
            .await
            .expect("An HTTP client error occured")
            .owner;

        println!("3");
        Bot { user, owner }
    })
    .await
}
Chayim Friedman
  • 47,971
  • 5
  • 48
  • 77
  • I went with a variant of your snippet : 1. declare a static BOT variable. 2. use serenity's `ready` event to initialize it on startup. 3. implement a `get_data` function to avoid having to `.get().unwrap()` everytime I need to access the data – Fointard Aug 20 '22 at 15:21