#How to pass State into async task

32 messages · Page 1 of 1 (latest)

rough crow
#

I'm trying to create a background tasks in my BE code that calls an async function in a loop and emits the returned value as an even to the FE. The async function depends on a string that's part of the app's State and I'm struggling to pass the string into the async task without running into thread-safety errors. Here's a MWE:

#[derive(Serialize, Clone)]
struct DeploymentMonitor {
    values: String,
}

impl DeploymentMonitor {
    async fn get(tag: &str) -> Result<Self, Box<dyn Error>> {
        Ok(Self { values: tag.into() })
    }
}

#[derive(Default)]
struct AppSettings {
    deployment_monitor_tag: Arc<Mutex<String>>,
}

#[tauri::command]
fn set_deployment_monitor_tag(tag: &str, settings: State<AppSettings>) {
    dbg!(&tag);
    let mut current_tag = settings.deployment_monitor_tag.lock().unwrap();
    *current_tag = tag.to_string();
}

#[tauri::command]
fn setup_update_loop(window: Window, settings: State<AppSettings>) {
    let tag = settings.deployment_monitor_tag.clone();
    tauri::async_runtime::spawn(async move {
        loop {
            if let Ok(deployment_monitor) = DeploymentMonitor::get(&*tag.lock().unwrap()).await {
                window.emit("update", deployment_monitor);
            }
            // TODO: sleep for 1 second
        }
    });
}


fn main() {
    tauri::Builder::default()
        .manage(AppSettings::default())
        .invoke_handler(tauri::generate_handler![set_deployment_monitor_tag])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

The error I get is that the future in the async block cannot be send between threads because the tag is not Send. How can I fix this error?

rough crow
#

Should the fields in my AppState be Arc<Mutex<> or should the whole AppState b e wrapped in a Arc<Mutex<> in the manage methods?

spare crystal
#

I discuss that a bit in my article on the topic. The State effectively gives you what you get from an Arc, so you can skip it and just have Mutex like you see me do

#

Also, generally speaking, you wouldn't make every single property an Arc<Mutex<T>>> or Mutex<T>, the properties would just be their direct properties, like String, and then you lock the entire state mutex when you access it

#

This is fine if you think you'll need to access that value for long periods of time across multiple threads

#[derive(Default)]
struct AppSettings {
    deployment_monitor_tag: Mutex<String>,
}

However, I would probably do this instead

#[derive(Default)]
struct AppSettings {
    deployment_monitor_tag: String,
}

fn main() {
    tauri::Builder::default()
        .manage(Mutex::new(AppSettings::default()))
        .invoke_handler(tauri::generate_handler![set_deployment_monitor_tag])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
rough crow
#

Don't I need to wrap it in an Arc as well? Since I'm passing it between threads because of the await points?

#

I I don't I still get the same error

#[derive(Serialize, Clone)]
struct DeploymentMonitor {
    values: String,
}

impl DeploymentMonitor {
    async fn get(tag: &str) -> Result<Self, Box<dyn Error>> {
        Ok(Self { values: tag.into() })
    }
}

#[derive(Default)]
struct AppSettings {
    deployment_monitor_tag: String,
}

#[tauri::command]
fn set_deployment_monitor_tag(tag: &str, settings: State<Mutex<AppSettings>>) {
    dbg!(&tag);
    let mut settings = settings.lock().unwrap();
    settings.deployment_monitor_tag = tag.to_string();
}

#[tauri::command]
fn setup_update_loop(window: Window, settings: State<'_, Mutex<AppSettings>>) {
    tauri::async_runtime::spawn(async move {
        loop {
            if let Ok(deployment_monitor) =
                DeploymentMonitor::get(&settings.lock().unwrap().deployment_monitor_tag).await
            {
                window.emit("update", deployment_monitor);
            }
            // TODO: sleep for 1 second
        }
    });
}

fn main() {
    tauri::Builder::default()
        .manage(Mutex::new(AppSettings::default()))
        .invoke_handler(tauri::generate_handler![set_deployment_monitor_tag])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
#

Hmm, when I wrap it in an Arc it's still not safe to pass between threads

#[tauri::command]
fn setup_update_loop(window: Window, settings: State<'_, Arc<Mutex<AppSettings>>>) {
    let settings = Arc::clone(&settings);
    tauri::async_runtime::spawn(async move {
        loop {
            if let Ok(deployment_monitor) =
                DeploymentMonitor::get(&settings.lock().unwrap().deployment_monitor_tag).await
            {
                window.emit("update", deployment_monitor);
            }
            // TODO: sleep for 1 second
        }
    });
}

fn main() {
    tauri::Builder::default()
        .manage(Arc::new(Mutex::new(AppSettings::default())))
        .invoke_handler(tauri::generate_handler![set_deployment_monitor_tag])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
spare crystal
# rough crow Hmm, when I wrap it in an `Arc` it's still not safe to pass between threads ```r...

Again, the Arc and the State effectively give you the same thing. An Arc is a reference counter, allowing you to create multiple references to the same value in order to make sure the value isn't dropped too soon. A State stores your value in a global state manager, which means the value won't be dropped until the program exits. So no, you don't need the Arc, because the issue it normally solves isn't relevant for a State

#

The only reason I use an Arc in my example is because I'm explicitly there showing off how to use a non-Tauri version of a state, in which case you would use an arc

#
#[tauri::command]
async fn setup_update_loop(app: AppHandle, window: Window) {
    let app = app.handle();
    tauri::async_runtime::spawn(async move {
        let state_mutex = app.state::<Mutex<AppSettings>>();
        loop {
            let state = state_mutex.lock().unwrap();
            if let Ok(deployment_monitor) =
                DeploymentMonitor::get(state.deployment_monitor_tag).await
            {
                window.emit("update", deployment_monitor);
            }
            std::thread::sleep(std::time::Duration::from_secs(1));
        }
    });
}
#

This is one approach you might explore

rough crow
#

Unfortunately that also gives the same error

spare crystal
polar plover
#

oh wait

#

i can't read

rough crow
#

I changed the code to this to fix the type error

#[tauri::command]
async fn setup_update_loop(app: AppHandle, window: Window) {
    tauri::async_runtime::spawn(async move {
        let app = app.app_handle();
        let state_mutex = app.state::<Mutex<AppSettings>>();
        loop {
            let state = state_mutex.lock().unwrap();
            if let Ok(deployment_monitor) =
                DeploymentMonitor::get(&state.deployment_monitor_tag).await
            {
                window.emit("update", deployment_monitor);
            }
            std::thread::sleep(std::time::Duration::from_secs(1));
        }
    });
}
#

Then the error is

error: future cannot be sent between threads safely
   --> src-tauri/src/main.rs:57:33
    |
57  |       tauri::async_runtime::spawn(async move {
    |  _________________________________^
58  | |         let app = app.app_handle();
59  | |         let state_mutex = app.state::<Mutex<AppSettings>>();
60  | |         loop {
...   |
68  | |         }
69  | |     });
    | |_____^ future created by async block is not `Send`
    |
    = help: within `[async block@src-tauri/src/main.rs:57:33: 69:6]`, the trait `Send` is not implemented for `std::sync::MutexGuard<'_, AppSettings>`
note: future is not `Send` as this value is used across an await
   --> src-tauri/src/main.rs:63:71
    |
61  |             let state = state_mutex.lock().unwrap();
    |                 ----- has type `std::sync::MutexGuard<'_, AppSettings>` which is not `Send`
62  |             if let Ok(deployment_monitor) =
63  |                 DeploymentMonitor::get(&state.deployment_monitor_tag).await
    |                                                                       ^^^^^ await occurs here, with `state` maybe used later
...
68  |         }
    |         - `state` is later dropped here
note: required by a bound in `tauri::async_runtime::spawn`
   --> /Users/alexander.vansaase/.cargo/registry/src/index.crates.io-6f17d22bba15001f/tauri-1.4.1/src/async_runtime.rs:270:15
    |
268 | pub fn spawn<F>(task: F) -> JoinHandle<F::Output>
    |        ----- required by a bound in this function
269 | where
270 |   F: Future + Send + 'static,
    |               ^^^^ required by this bound in `spawn`
polar plover
#

using tokio's mutex should make it work

#

or at least get rid of some of these errors

rough crow
#

That was it

#

Thank you both very much

#

Complete code

use tokio::sync::Mutex;

#[derive(Serialize, Clone)]
struct DeploymentMonitor {
    values: String,
}

impl DeploymentMonitor {
    async fn get(tag: &str) -> Result<Self, Box<dyn Error>> {
        Ok(Self { values: tag.into() })
    }
}

#[derive(Default)]
struct AppSettings {
    deployment_monitor_tag: String,
}

#[tauri::command]
async fn set_deployment_monitor_tag(
    tag: &str,
    settings: State<'_, Mutex<AppSettings>>,
) -> Result<(), ()> {
    dbg!(&tag);
    let mut settings = settings.lock().await;
    settings.deployment_monitor_tag = tag.to_string();
    Ok(())
}

#[tauri::command]
async fn setup_update_loop(app: AppHandle, window: Window) {
    tauri::async_runtime::spawn(async move {
        let app = app.app_handle();
        let state_mutex = app.state::<Mutex<AppSettings>>();
        loop {
            let state = state_mutex.lock().await;
            if let Ok(deployment_monitor) =
                DeploymentMonitor::get(&state.deployment_monitor_tag).await
            {
                window.emit("update", deployment_monitor).unwrap();
            }
            std::thread::sleep(std::time::Duration::from_secs(1));
        }
    });
}

fn main() {
    tauri::Builder::default()
        .manage(Mutex::new(AppSettings::default()))
        .invoke_handler(tauri::generate_handler![set_deployment_monitor_tag])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
spare crystal
#

@rough crow Just wanted to link you to this part of my article btw. It's worth reading if you're gonna use the tokio mutex. I'm not saying you should change your approach, because honestly the gain is probably not worth it for you, but technically the std version is better to use whenever possible, and in most cases (according to tokio themselves) if you can't use the std version, there's other approaches that are better to use than their mutex
http://127.0.0.1:7684/tauri_concepts/state_management.html#async-vs-sync-mutex

rough crow
#

Thanks Il take a look at that (and the rest of your website)

spare crystal
#

The rest is really underdeveloped, still working hard on it, that article is the only one that's anywhere close to finished x)

rough crow
spare crystal
# rough crow I had a closer look at the article. At https://simonhyll.github.io/tauri-by-simo...

Well, off the top of my head, you can use once_cell to create a variable that can only ever be assigned to once. When you run a function like the setup function I have in that example you can flip it to true

use once_cell::sync::OnceCell;
static MAIN_WINDOW_SETUP: OnceCell<bool> = OnceCell::new();
// In function
MAIN_WINDOW_SETUP.set(true).unwrap();
// Then you can check if it's true or not
if MAIN_WINDOW_SETUP.get().unwrap() {
// Or, in a blocking manner
if MAIN_WINDOW_SETUP.wait() {

https://docs.rs/once_cell/latest/once_cell/sync/struct.OnceCell.html

Another alternative is to use a State where you just default all windows to be false, then set them to true and check at the start of the setup function whether it's true or false

struct WindowSetupState {
    main_window: bool,
    second_window: bool,
}
rough crow
#

I found a third way. Store an optional JoinHandle of the task in the app state. In the command that starts the update loop I only spawn the task if the option is none. This allows me to also stop the loop with another command.