Impaled on the horns of a lifetime

It's the usual sort of error...

error: lifetime may not live long enough
   --> src\main.rs:619:59
    |
619 |           with_browser(cv, port, es, move |cv, client, _es| async move {
    |  ______________________________________________------_____-_^
    | |                                              |          |
    | |                                              |          return type of closure `impl Future<Output = Result<(), DewError>>` contains a lifetime `'2`
    | |                                              has type `&'1 fantoccini::Client`
620 | |             println!("host = {:#?}", cv.get("host"));
621 | |             es.push_simple("Whatever, dude.");
622 | |
...   |
626 | |             Ok(())
627 | |         }).await
    | |_________^ returning this value requires that `'1` must outlive `'2`

This starts a WebDriver then calls the call_me closure...

async fn with_webdriver<'cv, 'es, F, Fut>(
    config: &'cv FrozenConfigValues,
    error_stack: &'es ErrorStack,
    call_me: F)
where
    F: FnOnce(&'cv FrozenConfigValues, u16, &'es ErrorStack) -> Fut,
    Fut: Future<Output = Result<(), DewError>>,
{
    if error_stack.keep_going() {
        let mut wrangler = WebDriverWrangler::new();
        let (mut ports, text) = wrangler.start().await;
        for line in text.into_iter() {
            error_stack.push_level(Level::Info, line);
        }
        match ports.len() {
            0 => error_stack.push_simple("no WebDrivers could be found".to_string()),
            1 => {}
            _ => {
                let mut rng = rand::thread_rng();
                ports.shuffle(&mut rng);
            }
        }
        if error_stack.keep_going() {
            error_stack.push_level(Level::Info, format!("using the {} WebDriver.", ports[0].0));

            // ----- call_me -----
            error_stack.check(call_me(config, ports[0].1, error_stack).await);
        }
        wrangler.stop().await;
    }
}

It compiles and works well.

In the with_webdriver closure I want to be able to call the function below. It prepares a fantoccini::Client then calls its call_me closure...

async fn with_browser<'cv, 'es, F, Fut>(
    config: &'cv FrozenConfigValues,
    port: u16,
    error_stack: &'es ErrorStack,
    call_me: F
) -> Result<(), DewError>
where
    F: FnOnce(&'cv FrozenConfigValues, &Client, &'es ErrorStack ) -> Fut,
    Fut: Future<Output = Result<(), DewError>>,
{
    if error_stack.keep_going() {
        let url = format!("http://localhost:{}", port);

        // ----- the troublemaker -----
        let client = ClientBuilder::native().connect(&url).await?;

        // ----- call_me -----
        error_stack.check(call_me(config, &client, error_stack).await);

        error_stack.check(client.close().await);
    }
    Ok(())
}

A usage example...

    with_webdriver(&config, &error_stack, move |cv, port, es| async move {
        with_browser(cv, port, es, move |cv, client, _es| async move {
            println!("host = {:#?}", cv.get("host"));
            es.push_simple("Whatever, dude.");

            // ----- the troublemaker -----
            client.goto("http://example.com/").await?;

            Ok(())
        }).await
    }).await;

If the "troublemaker" line is removed from the example, the code compiles and works. Obviously, I need that line there. Being able to use client is the whole point!

I tried tying the &Client lifetime to the returned closure lifetime with this...

async fn with_browser<'cv, 'es, F, Fut>(
    config: &'cv FrozenConfigValues,
    port: u16,
    error_stack: &'es ErrorStack,
    call_me: F
) -> Result<(), DewError>
where
    F: for <'c> FnOnce(&'cv FrozenConfigValues, &'c Client, &'es ErrorStack ) -> Fut + 'c,
    Fut: Future<Output = Result<(), DewError>>,

The compiler doesn't like that... error[E0261]: use of undeclared lifetime name 'c`` ...and it makes a suggestion to declare the lifetime with the function declaration. That seems to be too much lifetime... error[E0597]: client does not live long enough.

I've tried everything I can find on the forum. None of what I can find is exactly the same problem so I could easily be messing up the changes. I've tried reading (and rereading) things like Higher-Rank Trait Bounds (HRTBs). I've run out of ideas.

Please help.

The simple answer would be to stick the Client in an Arc or something if possible. You could also consider just passing the Client by value. It looks like you have some cleanup work you want to do, but you may be able to do that with a wrapper type that implements Drop.

You're having problems because async functions in Rust just return a Future that "contains" the code inside them. Because of that you're indirectly trying to return a reference to a local value. with_browser owns the client but it's also trying to return a future that contains a reference to the client.

More async details

Edit: I don't think this was entirely correct. Don't think about lifetimes after midnight

The following code doesn't work for similar reasons, though the async case hides the details in a way that can make it harder to reason about.

fn bad_ref() -> &u8 {
    let value = 1u8;
    &value
}
1 Like

Thank you for the reply!

Yeah. That's not going to work. Client::close consumes the Client...

error[E0507]: cannot move out of an `Arc`
   --> src\main.rs:491:27
    |
491 |         error_stack.check(client.close().await);
    |                           ^^^^^^^-------
    |                           |      |
    |                           |      value moved due to this method call
    |                           move occurs because value has type `fantoccini::Client`, which does not implement the `Copy` trait

Is there a way to .await in a Drop implementation?


What confuses me is that I had the some problem with the other two parameters (config and error_stack). Giving them explicit lifetimes convinced the compiler they would survive longer than the Futures. Why can't I do the same for the Client reference?

I assume the lifetime of &client is limited to this line because of the .await...

        // ----- call_me -----
        error_stack.check(call_me(config, &client, error_stack).await);

If that's true then how do I convince the compiler that's true?

I think I need a longer lifetime than 'c when 'c is declared here...

for <'c> F: FnOnce(&'cv FrozenConfigValues, &Client, &'es ErrorStack ) -> Fut,

But a shorter lifetime than 'c when 'c is declared for the function...

async fn with_browser<'cv, 'c, 'es, F, Fut>(

Unfortunately not at the moment. If you're using a runtime like tokio you can spawn the future on a new task but that won't guarantee that the task finished before the drop call returns

It worked for the other two because they're being passed in as references from the beginning. The problem here is that you're constructing client inside one of the futures and then capturing the reference in another future

Have you tried forcing the closure's future to return the Client back to you then?

1 Like

While that should work it's also going to make the question mark operator essentially impossible to use inside the closure.


While fiddling with BoxFuture...

^ returning this value requires that '1 must outlive 'static

Uh. Must out live 'static? So, like, outlast the running program? :grin:

Why would that be the case? I just mean return Result<Client, Error> so you can recover the client by value.

If the closure returns an error...

    with_webdriver(&config, &error_stack, move |cv, port, es| async move {
        with_browser(cv, port, es, move |cv, client, _es| async move {

            client.goto("http://example.com/").await?;

            // If the line above returns an error then we won't reach...
            Ok(client)
        }).await
    }).await;

Then the Client won't be returned to with_browser which won't be able to call close...

async fn with_browser<'cv, 'es, F, Fut>(
    config: &'cv FrozenConfigValues,
    port: u16,
    error_stack: &'es ErrorStack,
    call_me: F
) -> Result<(), DewError>
where
    F: FnOnce(&'cv FrozenConfigValues, Client, &'es ErrorStack ) -> Fut,
    Fut: Future<Output = Result<Client, DewError>>,
{
    if error_stack.keep_going() {
        let url = format!("http://localhost:{}", port);
        let client = ClientBuilder::native().connect(&url).await?;

        match call_me(config, client, error_stack).await {
            Ok(client) => {
                // ...which means we won't reach this...
                error_stack.check(client.close().await);
            }
            Err(error) => {}
        }
    }
    Ok(())
}

Oh of course :person_facepalming:

Another options is to just make it the caller's job to perform the cleanup. You can use the type system to more or less guarantee that they don't forget too

Playground

use anyhow::Result;
use std::{future::Future, marker::PhantomData};

#[tokio::main]
async fn main() {
    with_browser(|client, cleanup| async move {
        client.goto("path").await?;

        cleanup.cleanup_client(client).await
    })
    .await
    .unwrap()
}

struct Client;

impl Client {
    async fn goto(&self, _path: &str) -> Result<()> {
        Ok(())
    }

    async fn close(self) -> Result<()> {
        Ok(())
    }
}

/// A struct that shouldn't be constructable by any function except `cleanup_client`.
///
/// This guarantees that the user calls the cleanup function
struct Finish(PhantomData<()>);

async fn with_browser<F, Fut>(func: F) -> Result<()>
where
    F: FnOnce(Client, Cleanup) -> Fut,
    Fut: Future<Output = Result<Finish>>,
{
    let client = Client;

    func(client, Cleanup).await?;

    Ok(())
}

/// Store whatever extra info you need to do your cleanup in this struct
struct Cleanup;

impl Cleanup {
    async fn cleanup_client(self, client: Client) -> Result<Finish> {
        client.close().await?;
        Ok(Finish(PhantomData))
    }
}
1 Like

As far as I can tell that doesn't guarantee close being called. Just that the user tried to call it. Playground

But, I think I figured out how to get it working. BoxFuture. I also merged with_webdriver and with_browser so I'd have fewer things in play.

async fn with_both<F>(
    config: &FrozenConfigValues,
    error_stack: &ErrorStack,
    call_me: F,
)
where
    for <'a> F: FnOnce(&'a FrozenConfigValues, &'a Client, &'a ErrorStack ) -> BoxFuture<'a, Result<(), DewError>>,
{
    if error_stack.keep_going() {
        let mut wrangler = WebDriverWrangler::new();
        let (mut ports, text) = wrangler.start().await;
        for line in text.into_iter() {
            error_stack.push_level(Level::Info, line);
        }
        match ports.len() {
            0 => error_stack.push_simple("no WebDrivers could be found".to_string()),
            1 => {}
            _ => {
                let mut rng = rand::thread_rng();
                ports.shuffle(&mut rng);
            }
        }
        if error_stack.keep_going() {
            error_stack.push_level(Level::Info, format!("using the {} WebDriver.", ports[0].0));

            if error_stack.keep_going() {
                let url = format!("http://localhost:{}", ports[0].1);
                if let Some(client) = error_stack.check(ClientBuilder::native().connect(&url).await) {
                    error_stack.check(call_me(config, &client, error_stack).await);
                    error_stack.check(client.close().await);
                }
            }
        }
        wrangler.stop().await;
    }
}

From the user's perspective the only thing that changes (other than it works) is the necessity to call boxed and one less wrapper.

    with_both(&config, &error_stack, move |cv, client, es| async move {
        println!("host = {:#?}", cv.get("host"));
        es.push_simple("Whatever, dude.");
        client.goto("http://example.com/").await?;
        client
            .wait()
            .at_most(Duration::from_secs(3))
            .every(Duration::from_millis(100))
            .for_url(url::Url::parse("https://whatever.com/")?)
            .await?;
        sleep(Duration::from_secs(5)).await;
        Ok(())
    }.boxed()).await;

One caveat: I had to make a few things Send + Sync. Apparently boxing your futures adds some restrictions.

In any case, @semicoleon, thank you for the help!

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.