Search code examples
rustasync-awaitlifetimerust-tokio

Why does tokio::spawn complain about lifetimes even with .clone()?


I am trying to compile the following seemingly straightforward code, but I'm getting an error:

use std::io::Error;

#[derive(Debug)]
struct NetworkConfig {
    bind: String,
    node_key_file: String,
}

async fn network_handler(network_config: &NetworkConfig) -> Result<(), Error> {
    Ok(())
}

async fn run(network_config: &NetworkConfig) -> Result<(), Error> {
    let network_config_copy = network_config.clone();
    tokio::spawn(async move {
        network_handler(&network_config_copy).await
    }).await?
}
error: cannot infer an appropriate lifetime
  --> src/network.rs:43:18
   |
43 | async fn run(network_config: &NetworkConfig) -> Result<(), Error> {
   |              ^^^^^^^^^^^^^^ ...but this borrow...
44 |     let network_config_copy = network_config.clone();
45 |     tokio::spawn(async move {
   |     ------------ this return type evaluates to the `'static` lifetime...
   |
note: ...can't outlive the lifetime `'_` as defined on the function body at 43:34
  --> src/network.rs:43:34
   |
43 | async fn run(network_config: &NetworkConfig) -> Result<(), Error> {
   |                              ^
help: you can add a constraint to the return type to make it last less than `'static` and match the lifetime `'_` as defined on the function body at 43:34
   |
45 |     tokio::spawn + '_(async move {
   |     ^^^^^^^^^^^^^^^^^

From the previous discussions and examples I have found on the subject, I understand that passing a reference to network_config to the spawned closure would cause lifetime problems since the separate thread may outlive network_config. This is why I am moving a clone of network_config to the spawned thread, but there still seems to be a lifetime ambiguity.

Is there any extra hint I could give the compiler so that it correctly gets the lifetimes? Or am I doing the whole thing wrong?


Solution

  • If you want clone the NetworkConfig you need to implement the Clone trait:

    #[derive(Debug, Clone)]
    struct NetworkConfig {
        bind: String,
        node_key_file: String,
    }
    

    Otherwise, for the rules of receiver method lookup you will end up with invoking a Clone on a reference through the following Clone implementer:

    impl<'_, T> Clone for &'_ T
    

    And the cloned reference will have a lifetime bound to scope of clone() invocation.

    With derive(Clone) the run function compiles, but it works only when network_config argument has 'static lifetime, because of tokio::spawn lifetime requirement.

    Probably this is not what you want. If this is the case pass NetworkConfig by value and eventually clone it in the caller context.

    use std::io::Error;
    
    #[derive(Debug, Clone)]
    struct NetworkConfig {
        bind: String,
        node_key_file: String,
    }
    
    async fn network_handler(network_config: &NetworkConfig) -> Result<(), Error> {
        println!("using {:?}", network_config);
        Ok(())
    }
    
    async fn run(network_config: NetworkConfig) -> Result<(), Error> {
        tokio::spawn(async move { network_handler(&network_config).await }).await?
    }
    
    #[tokio::main]
    async fn main() {
        let config = config::NetworkConfig {
            bind: "my_bind".to_owned(),
            node_key_file: "abc".to_owned(),
        };
    
        tokio::spawn(run(config.clone()));
    }
    

    You may ask why this works, indeed a reference is still passed to network_handler().

    This is because network_config is moved inside the spawn async block and this makes gaining static lifetime for the inferred type of the async block.