#Initializing a static asynchronously in test context

28 messages · Page 1 of 1 (latest)

blazing rampart
#

hello, im having trouble to initialize a static variable only once asynchronously in a test context. this code helps to understand the issue:

// once_cell::OnceCell or std::sync::OnceLock dont work
static FOO: OnceCell<i32> = OnceCell::new();

fn bar(x: i32) {
  // function that must be called **only once**
}

async fn get_foo() -> i32 {
  if let Some(foo) = FOO.get() {
    return foo;
  }

  let x = big_computation().await;
  FOO.set(x).unwrap();
  bar(x);
  x
}

#[tokio::test]
async fn test1() {
  let x = get_foo().await;
  // ...
}

#[tokio::test]
async fn test2() {
  let x = get_foo().await;
  // ...
}
```here, `bar(x);` is called twice, whereas the code in which it's called should be reached only once. how to handle this?

thanks :D
dusky hill
#

It's weird that you want bar to only run once. Usually people want big_computation to only run once. But if so, you can use get_or_init.

blazing rampart
#

well, i named the func big_computation but its a small func actually lol, i also want it to run once, in fact i want all this block:

let x = big_computation().await;
FOO.set(x).unwrap();
bar(x);
```to run once
#

and i tested FOO.get_or_init(|| x) but its still called twice :(

dusky hill
#

It would be FOO.get_or_init(|| { bar(x); x })

blazing rampart
#

yea i guess i'll have to use this

dusky hill
#
static FOO: async_once_cell::OnceCell<i32> = async_once_cell::OnceCell::new();

async fn get_foo() -> i32 {
  *FOO.get_or_init(async {
      let x = big_computation().await;
      bar(x);
      x
  }).await
}
blazing rampart
#

i was wondering, can i use a Mutex<Option<i32>> instead?

dusky hill
#

you can't use std's mutex since there's async work to do while the lock is being held, but tokio's mutex will work. It's just more expensive.

blazing rampart
#

im having this error using async_once_cell: "A Tokio 1.x context was found, but it is being shutdown."

#

i had the same error when using tokio::sync::Mutex<Option<i32>>

#

and i cant understand from where its coming from

dusky hill
#

Hm, I guess this is because each test gets its own tokio runtime. I dunno if there's a way to make this work with the tokio::test macro. You could make the runtime in the (non-async) cell and then use that.

#

Like this

use std::sync::LazyLock;
use tokio::runtime::{Builder, Runtime};

static FOO: LazyLock<(i32, Runtime)> = LazyLock::new(|| {
    let rt = Builder::new_current_thread().build().unwrap();
    let x = rt.block_on(big_computation());
    bar(x);
    (x, rt)
});

async fn big_computation() -> i32 {
    5
}

fn bar(_x: i32) {}

#[test]
fn test1() {
    let &(x, ref rt) = &*FOO;
    rt.block_on(async {
        // do all work in here
        println!("{x}");
    });
}
blazing rampart
#

okay this is getting a bit complicated xd

#

i just modified the function bar(x: i32) so that it ignores being called many times (it was panicking before in this case)

#

thanks for the help tho, and sorry i know i should've told you from the beginning that i can modify the bar function

#

i was trying to find a way to make this work without modifying it, but in the end i'll have to xd

dusky hill
#

what kinda things are you doing that you want to share between tests?

blazing rampart
#

environment variables xd

#

in the program, im retrieving them once and using them accross every modules with a call like env().foobar_var

dusky hill
#

I'd probably just have every test retrieve them. It's better to have simple, isolated tests than fast, interdependent tests.

blazing rampart
#

well the goal wasn't actually for the tests themselves, i dont mind if the tests are slow, it was for the program im testing. the function that retrieves all the env vars in the example was bar(x: i32). it initializes a static variable which contains the values of all the environment variables, so that they're accessible through env().foobar. so i needed a way to call bar(...); in the tests too

#

lets rename bar to init_env, the code was like this before:

static ENV: OnceCell<AppEnv> = OnceCell::new();

fn env() -> &'static AppEnv {
  ENV.get().expect("env not initialized")
}

fn init_env(env: AppEnv) {
  ENV.set(env).unwrap_or_else(|| panic!("already init"));
}
```so i just removed the panic section and ignore the next calls:
```rs
fn init_env(env: AppEnv) {
  let _ = ENV.set(env);
}
#

the panic was the reason i couldn't call init_env (or bar) twice

#

sry if all this sounds messy but its pretty simple when u got all the elements xd