Hi guys, I want to make a tonic rpc method retriable. A common rpc method has mutable refrence of client, for example:
async fn hello(client: &mut Client, request: Request) -> Response {
/// do something
}
Further, I need to store "hello" function in a wrapper, then I can use it multiple times. for example
pub struct RpcRetryWrapper<C, I, O, F> {
f: F,
marker: std::marker::PhantomData<fn(&mut C, I) -> O>,
}
impl<C, I, O, F> RpcRetryWrapper
where
F: // trait bound
{
async fn call_once(&mut self, client: &mut C, request: I) -> O {
// do something
}
async fn call_twice(&mut self, client: &mut C, request: I) -> O
where
I: Clone
{
let first_res = self.call_once(client, request.clone());
let second_res = self.call_once(client, request);
// do something
}
}
However, there is a question that I can not find correct trait bound of F. I guess the trait bound is something like this, but compiler will report an error.
F: for<'a> Fn(&'a mut C, I) -> impl Future<Output = O> + 'a
So, what trait bound can describe a async function that capcuture reference of a variable?
Just treat O
as the future type and use <O as Future>::Output
to get the final output of the async function.This may not fix all of your problems, but it does fix the immediate errors in your example
use std::future::Future;
pub struct RpcRetryWrapper<C, I, O, F> {
f: F,
marker: std::marker::PhantomData<fn(&mut C, I) -> O>,
}
impl<C, I, O, F> RpcRetryWrapper<C, I, O, F>
where
F: for<'a> Fn(&'a mut C, I) -> O,
O: Future,
{
async fn call_once(&mut self, client: &mut C, request: I) -> O::Output {
// do something
todo!()
}
async fn call_twice(&mut self, client: &mut C, request: I) -> O::Output
where
I: Clone,
{
let first_res = self.call_once(client, request.clone()).await; // await necessary to avoid a borrow checker error
let second_res = self.call_once(client, request);
// do something
todo!()
}
}
1 Like
Thanks, I modify codes following your suggestions.
but compiler will report error when i try to use call_once().
use std::future::Future;
pub struct RpcRetryWrapper<C, I, F> {
pub f: F,
marker: std::marker::PhantomData<fn(&mut C, I)>,
}
impl<C, I, F> RpcRetryWrapper<C, I, F> {
fn new(f: F) -> Self {
Self {
f,
marker: std::marker::PhantomData,
}
}
}
impl<C, I, O, F> RpcRetryWrapper<C, I, F>
where
I: Clone,
F: for<'a> Fn(&'a mut C, I) -> O,
O: Future,
{
async fn call_once(&self, client: &mut C, request: I) -> O::Output {
(self.f)(client, request).await
}
async fn call_twice(&self, client: &mut C, request: I) -> O::Output {
let first_res = (self.f)(client, request.clone()).await;
let second_res = (self.f)(client, request.clone()).await;
second_res
}
}
#[cfg(test)]
mod tests {
use crate::{call_exmaple, RpcRetryWrapper};
#[tokio::test]
async fn it_works() {
let mut client = "client".to_string();
let request = "hello".to_string();
let wrapper: RpcRetryWrapper<String, String, _> = RpcRetryWrapper::new(call_exmaple);
wrapper.call_once(&mut client, request); // error reported here.
}
}
impl<C, I, O, F> RpcRetryWrapper<C, I, F>
where
I: Clone,
F: for<'a> Fn(&'a mut C, I) -> O,
O: Future,
async fn
outputs capture their input lifetimes, and so they cannot be represented by a type parameter like O
when there is an input lifetime, as O
must resolve to a single concrete type. The returned Future
s are not all the same single type; they vary by lifetime.
There are verbose ways to try and get around that restriction in some cases, but last I knew they don't work for the async fn
case (the compiler can't yet see through the indirection and too many higher-ranked bounds).
3 Likes
Here is my another try to solve this problem.
- construct a concrete type
LifetimeBoundFutureWrapper
for output future of async fn
which capture input lifetimes. it seems ok to convert output of call_example
into a LifetimeBoundFutureWrapper<'_, String, _>
.
trait LifetimeBoundFuture<'a, T>: 'a + Future<Output = T> {}
impl<'a, T, F> LifetimeBoundFuture<'a, T> for F where F: 'a + Future<Output = T> {}
struct LifetimeBoundFutureWrapper<'a, T, F> {
fut: F,
marker: std::marker::PhantomData<fn() -> &'a T>,
}
impl<'a, T, F> From<F> for LifetimeBoundFutureWrapper<'a, T, F>
where
F: LifetimeBoundFuture<'a, T>,
{
fn from(fut: F) -> Self {
Self {
fut,
marker: std::marker::PhantomData,
}
}
}
async fn call_exmaple(client: &mut String, request: String) -> String {
request
}
#[cfg(test)]
mod tests {
use crate::{call_exmaple, LifetimeBoundFutureWrapper, RpcRetryWrapper};
#[tokio::test]
async fn future_wrapper_test() {
let mut client = "client".to_string();
let request = "hello".to_string();
let fut_wrapper: LifetimeBoundFutureWrapper<'_, String, _> =
call_exmaple(&mut client, request).into();
}
}
- Modify trait bound in
RpcRetryWrapper
impl<C, I, O, F> RpcRetryWrapper<C, I, F>
where
I: Clone,
F: for<'a> Fn(&'a mut C, I) -> LifetimeBoundFutureWrapper<'a, O::Output, O>,
O: Future,
- Finally, how to prove that
call_example
satisfy the trait bound. All i knew is to define an immediate trait like AsyncFuncExt
to do blanket implementation for all async fn
of this form.