Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to bind lifetimes of Futures to fn arguments in Rust

Im trying to write a simple run_transaction function for the Rust MongoDB Driver

This function tries to execute a transaction through the mongo db client and retries the transaction if it encounters a retryable error

Here is a minimum reproducible example of the function.

use mongodb::{Client, Collection, ClientSession};
use mongodb::bson::Document;
use std::future::Future;

pub enum Never {}

fn main() {
    run_transaction(|mut session| async move {
        let document = collection().find_one_with_session(None,  None, &mut session).await?.unwrap();
        let r: Result<Document, TransactionError<Never>> = Ok(document);
        return r;
    });
}

fn collection() -> Collection<Document> {
    unimplemented!();
}

fn client() -> Client {
    unimplemented!();
}

pub enum TransactionError<E> {
    Mongodb(mongodb::error::Error),
    Custom(E)
}

impl<T> From<mongodb::error::Error> for TransactionError<T> {
    fn from(e: mongodb::error::Error) -> Self {
        TransactionError::Mongodb(e)
    }
}

// declaration
pub async fn run_transaction<T, E, F, Fut>(f: F) -> Result<T, TransactionError<E>> 
  where for<'a>
        F: Fn(&'a mut ClientSession) -> Fut + 'a,
        Fut: Future<Output = Result<T, TransactionError<E>>> { 

  
  let mut session = client().start_session(None).await?;
  session.start_transaction(None).await?;
      
  'run: loop {
    let r = f(&mut session).await;

    match r {
      Err(e) => match e {
        TransactionError::Custom(e) => return Err(TransactionError::Custom(e)),
        TransactionError::Mongodb(e) => {
          if !e.contains_label(mongodb::error::TRANSIENT_TRANSACTION_ERROR) {
            return Err(TransactionError::Mongodb(e));
          } else {
            continue 'run;
          }
        }
      },

      Ok(v) => {
        'commit: loop {
          match session.commit_transaction().await {
            Ok(()) => return Ok(v),
            Err(e) => {
              if e.contains_label(mongodb::error::UNKNOWN_TRANSACTION_COMMIT_RESULT) {
                continue 'commit;
              } else {
                return Err(TransactionError::Mongodb(e))
              }
            }
          }
        }
      }
    }
  }
}

But the borrow checker keeps complaining with this message:

error: lifetime may not live long enough
  --> src/main.rs:8:35
   |
8  |       run_transaction(|mut session| async move {
   |  ______________________------------_^
   | |                      |          |
   | |                      |          return type of closure `impl Future` contains a lifetime `'2`
   | |                      has type `&'1 mut ClientSession`
9  | |         let document = collection().find_one_with_session(None,  None, &mut session).await?.unwrap();
10 | |         let r: Result<Document, TransactionError<Never>> = Ok(document);
11 | |         return r;
12 | |     });
   | |_____^ returning this value requires that `'1` must outlive `'2`

Is there a way I can solve this?

like image 718
Ramiro Aisen Avatar asked Dec 04 '25 06:12

Ramiro Aisen


1 Answers

What you really need is something like:

pub async fn run_transaction<T, E, F, Fut>(f: F) -> Result<T, TransactionError<E>> 
  where
    for<'a>
        F: Fn(&'a mut ClientSession) -> Fut,
        Fut: Future<Output = Result<T, TransactionError<E>>> + 'a { 

Unfortunately this does not work because the "Higer Rank Trait Bound" (HRTB) defined in for<'a> only applies to the very next bound, not to every one of them, and there is no way to connect both lifetimes...

But not everything is lost! I've found this question in the Rust support forum with a similar problem, that can be adapted to your issue. The basic idea is that you create a trait that wraps the Fn and the Future bounds with the same lifetime:

pub trait XFn<'a, I: 'a, O> {
  type Output: Future<Output = O> + 'a;
  fn call(&self, session: I) -> Self::Output;
}

impl<'a, I: 'a, O, F, Fut> XFn<'a, I, O> for F
where
  F: Fn(I) -> Fut,
  Fut: Future<Output = O> + 'a,
{
  type Output = Fut;
  fn call(&self, x: I) -> Fut {
      self(x)
  }
}

And now your bound function is trivial:

pub async fn run_transaction<T, E, F>(f: F) -> Result<T, TransactionError<E>>
  where for<'a>
        F: XFn<'a, &'a mut ClientSession, Result<T, TransactionError<E>>>

Just remember that to call the function you have to write f.call(&mut session).

Unfortunately, the call to run_transaction(), as it is, does not compile, saying that implementation of FnOnce is not general enough. I think that it is a limitation/bug of the async move as async closures are unstable. But you can instead use a proper async function:

    async fn do_the_thing(session: &mut ClientSession) -> Result<Document, TransactionError<Never>> {
      let document = collection().find_one_with_session(None,  None, session).await?.unwrap();
      let r: Result<Document, TransactionError<Never>> = Ok(document);
      return r;
    }
    run_transaction(do_the_thing).await;

If you think that this is too complicated, and you don't mind paying the very small runtime price there is a simpler option: you can box the returned future, avoiding the second generic altogether:

pub async fn run_transaction<T, E, F>(f: F) -> Result<T, TransactionError<E>>
  where for<'a>
        F: Fn(&'a mut ClientSession) -> Pin<Box<dyn Future<Output = Result<T, TransactionError<E>>> + 'a>>

And then, to call it:

    run_transaction(|mut session| Box::pin(async move {
        let document = collection().find_one_with_session(None,  None, session).await?.unwrap();
        let r: Result<Document, TransactionError<Never>> = Ok(document);
        return r;
    }));
like image 77
rodrigo Avatar answered Dec 07 '25 05:12

rodrigo



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!