I have a struct Foo
with an async
method which may fail (returning a bool
). Therefore, I want to retry this operation()
in a loop until it succeeds. There's some extra boilerplate (e.g. throttling retries) which isn't relevant here, so let's assume a simple retry loop {}
.
If I directly call self.operation().await
in a loop it works fine. But I'd like to introduce a helper function, generic over the exact operation, to implement the retry loop (and the throttling boilerplate). This would avoid copy-pasting the retry loop, as in practice I have various operations to retry.
However, I couldn't figure out how to precisely declare this helper function in terms of types and lifetimes to get it to compile.
What I've tried so far:
use std::future::Future;
struct Foo {}
impl Foo {
// Operation that may fail and should be retried.
async fn operation(&mut self) -> bool {
unimplemented!()
}
// Directly retry the operation in a loop.
async fn direct(&mut self) {
loop {
if self.operation().await {
return;
}
}
}
// Retry the operation via a retry_loop helper function.
async fn indirect_1(&mut self) {
self.retry_loop_1(Foo::operation).await
}
async fn indirect_2(&mut self) {
self.retry_loop_1(|this| this.operation()).await
}
// First attempt, without lifetimes.
async fn retry_loop_1<F, Fut>(&mut self, mut f: F)
where
F: FnMut(&mut Self) -> Fut,
Fut: Future<Output = bool>,
{
loop {
if f(self).await {
return;
}
}
}
async fn indirect_3(&mut self) {
self.retry_loop_2(|this| this.operation()).await
}
// Second attempt, with lifetimes.
async fn retry_loop_2<'a, 'b, F, Fut>(&'a mut self, mut f: F)
where
'a: 'b,
F: FnMut(&'b mut Foo) -> Fut,
Fut: Future<Output = bool> + 'b,
{
loop {
if f(self).await {
return;
}
}
}
}
And the compiler errors:
error: implementation of `std::ops::FnOnce` is not general enough
--> src/foo.rs:22:14
|
22 | self.retry_loop_1(Foo::operation).await
| ^^^^^^^^^^^^ implementation of `std::ops::FnOnce` is not general enough
|
= note: `std::ops::FnOnce<(&'0 mut foo::Foo,)>` would have to be implemented for the type `for<'_> fn(&mut foo::Foo) -> impl futures::Future {foo::Foo::operation}`, for some specific lifetime `'0`...
= note: ...but `std::ops::FnOnce<(&mut foo::Foo,)>` is actually implemented for the type `for<'_> fn(&mut foo::Foo) -> impl futures::Future {foo::Foo::operation}`
error: lifetime may not live long enough
--> src/foo.rs:26:34
|
26 | self.retry_loop_1(|this| this.operation()).await
| ----- ^^^^^^^^^^^^^^^^ returning this value requires that `'1` must outlive `'2`
| | |
| | return type of closure is impl futures::Future
| has type `&'1 mut foo::Foo`
error[E0499]: cannot borrow `*self` as mutable more than once at a time
--> src/foo.rs:54:18
|
47 | async fn retry_loop_2<'a, 'b, F, Fut>(&'a mut self, mut f: F)
| -- lifetime `'b` defined here
...
54 | if f(self).await {
| --^^^^-
| | |
| | mutable borrow starts here in previous iteration of loop
| argument requires that `*self` is borrowed for `'b`
Note: I've here tried to minimize the problem - for more context the full code I want to refactor is in this code on GitHub.