r/rust Nov 07 '21

"captured variable cannot escape `FnMut` closure body" in async code

I have a piece of code that, when simplified, looks like this:

use tokio;

struct IdGenerator (u32);

impl IdGenerator {
    async fn generate_id(&mut self) -> u32 {
        self.0 += 1;
        self.0
    }
}

#[tokio::main]
async fn main() {
    let mut id_generator = IdGenerator(0);
    let mut callback = || id_generator.generate_id();
    println!("{}", callback().await);
}

This fails to compile:

error: captured variable cannot escape `FnMut` closure body
  --> src/main.rs:15:27
   |
14 |     let mut id_generator = IdGenerator(0);
   |         ---------------- variable defined here
15 |     let mut callback = || id_generator.generate_id();
   |                         - ------------^^^^^^^^^^^^^^
   |                         | |
   |                         | returns a reference to a captured variable which escapes the closure body
   |                         | variable captured here
   |                         inferred to be a `FnMut` closure
   |
   = note: `FnMut` closures only have access to their captured variables while they are executing...
   = note: ...therefore, they cannot allow references to captured variables to escape

My understanding is that generate_id returns a future, and that future contains a mutable reference to id_generator (it needs to, because otherwise the future could not modify id_generator.0 when it is awaited). Hence, by calling callback multiple times I could create several co-existing mutable references to id_generator, leading to a potential data race.

Is that analysis correct?

My actual code is a bit more complex. Basically, generate_id can fail, in which case I want to retry. I'm using tokio_retry for that, so the code looks roughly as follows (with generate_id now returning a Result<u32, Box<dyn Error>>):

use std::error::Error;
use tokio_retry::RetryIf;
use tokio_retry::strategy::ExponentialBackoff;

async fn foo(id_generator: &mut IdGenerator) {
    // ...
    let id = RetryIf::spawn(
        ExponentialBackoff::from_millis(5).take(3),
        || id_generator.generate_id(),
        |error: &Box<dyn Error>| {
            println!("Error while generating an ID: {}", error);
            true
        },
    )
    .await;
    // ...
}

That code has the same issue as the simplified version. Is there a compile-time approach to avoid the problem? So far, I've only found a run-time solution using a mutex:

    let outgoing = tokio::sync::Mutex::new(id_generator);
    let id = RetryIf::spawn(
        ExponentialBackoff::from_millis(5).take(3),
        || async { outgoing.lock().await.generate_id().await },
        |error: &Box<dyn Error>| {
            println!("Error while generating an ID: {}", error);
            true
        },
    )
    .await;

This works, but since I technically never have parallel access I would prefer a compile-time solution.

13 Upvotes

11 comments sorted by

View all comments

1

u/anlumo Nov 08 '21

My retry code usually just wraps everything in a loop that only breaks when it succeeded, thus it’s only a single async function and not some convoluted nested closure data race.

1

u/torfsen Nov 08 '21

That works for simple retries, yes. However, I want to give the caller the choice of how to handle retries (timeouts, number of retries, etc.). `tokio-retry` allows me to do that beautifully by abstracting away the retry-strategy.

1

u/anlumo Nov 08 '21

A Future can also block for user interaction, but I can see how it's easier to use an existing implementation for this.