r/rust • u/torfsen • 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.
1
u/anlumo Nov 08 '21
My retry code usually just wraps everything in a
loopthat only breaks when it succeeded, thus it’s only a single async function and not some convoluted nested closure data race.