In the past I've written wrappers around backoff::retry to cleanly encapsulate retry behavior, such as with reqwest:
pub fn get<T: DeserializeOwned>(url: &str, token: &str) -> Result<T, String> {
let op = || {
Client::new()
.get(url)
.headers(headers(token))
.send()
.map_err(backoff::Error::transient)
};
call(op)
}
fn call<T, F, E>(op: F) -> Result<T, String>
where
T: DeserializeOwned,
F: FnMut() -> Result<Response, Error<E>>,
E: Display,
{
retry(backoff(), op)
.map_err::<String, _>(|e| e.to_string())
.and_then(|mut response| {
let mut buf = String::new();
response
.read_to_string(&mut buf)
.expect("HTTP response not valid UTF-8");
if buf.contains("error") {
Err(format!("Received Error Response: {}", buf))
} else {
serde_json::from_str(&buf)
.map_err(|e| format!("Could not parse response body: {:?}", e))
}
})
}
I wanted to do something similar with gRPC now.
But gRPC in Rust means Tonic which means async.
I can write this easily enough:
pub fn retry<T, F>(mut op: F) -> Result<T, backoff::Error<String>>
where
F: FnMut() -> Result<T, String>,
{
let op2 = || {
block_in_place(|| Handle::current().block_on(async { op() }))
.map_err(backoff::Error::transient)
};
backoff::retry(backoff::ExponentialBackoff::default(), op2)
}
But that's written for a non-async op. gRPC service calls are async.
Conceptually, I want to do something like
pub fn retry<T, F>(mut op: F) -> Result<T, backoff::Error<String>>
where
F: **async** FnMut() -> Result<T, String>,
{
let op2 = || {
block_in_place(|| Handle::current().block_on(op()))
.map_err(backoff::Error::transient)
};
backoff::retry(backoff::ExponentialBackoff::default(), op2)
}
But, of course, this is not possible - async doesn't work that way.
I'm pretty sure, based on what I've just read, that there's just no way to desugar async into some kind of trait bounds generic enough to use with any lambda - no way to define a function of async A -> B, etc. Because async fns get turned into state machines.
Is that right? Is this hopeless?