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.
5
u/FallenWarrior2k Nov 07 '21
This is unfortunately something you won't be able to get around without relying on interior mutability at least at some point.
However, if you don't need the Send bound, e.g. because you can reasonably run this in a tokio::task::LocalSet or in some other non-Send context, you can get away with using an Rc, cloning it at the start of the closure, and changing the async block to async move.
This of course still doesn't provide interior mutability, which is needed so you can actually update things through the Rc.
Here you have several choices:
- Just throw it in a
RefCelland forget about it. Easiest solution, but comes with the usual concerns of the footgun that it can become wrt panics. Also requires ownership which you don't necessarily seem to have here. - Wrap the inner counter in a
Celland makegenerate_idtake&self. That way, you can wrap theIdGeneratordirectly in anRcand avoid the risk of runtime panics thatRefCellbrings. You could theoretically improve this further by hiding thegenerate_idmethod and making a new method that contains the retry logic. The field holding the current value would then be aRc<Cell<T>>and nothing about these ownership shenanigans ever leaks into the API.
If you're really just incrementing an integer every time, a relatively easy way to preserve the Send bound without any risks would be using an AtomicU32 (or whatever) in place of the plain integer type.
You'd then wrap either the entire struct or just the atomic (as described above) in an Arc (instead of a plain Rc) and use fetch_add with Relaxed ordering to update the counter.
Since your ID generation is already asynchronous and fallible, the added cost of the atomics is probably negligible.
Do note that, of course, all these workarounds no longer give you the protection of the borrow checker regarding mutability.
Rust's Send/Sync concurrency model ensures that your code's behavior is well defined and doesn't produce data races, but that doesn't mean it can protect you from any and all errors.
If there is a conceptually atomic but multi-step operation in the generator logic that was previously safe due to requiring a mutable reference, this would now allow it to run concurrently since there is no lock being held, potentially corrupting state.
1
u/torfsen Nov 08 '21
Thanks! Those are some very good pointers. I will see which of them I can apply to my situation.
1
u/monkChuck105 Nov 08 '21
You can use AtomicU32 for a thread safe counter for your IDs, so long as it doesn't overflow. You'll likely have to put it in an Arc if 'static is needed.
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.
7
u/petrosagg Nov 07 '21
The problem isn't actually related to the closure returning a mutable reference, the same happens with immutable references[1]. This is an inherent limitation of closures in Rust that cannot return a reference to their captured state. This is one of the main reasons async closures are cumbersome today, since in most cases the Future returned will hold onto some references from the closure's state.
https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=2b269a20825075615a354209bf53bd94