Xuanwo/backon

How to pass parameters to function call

yonas opened this issue · 13 comments

yonas commented

let content = fetch.retry(&ExponentialBuilder::default()).await?;

How do we pass parameters to function call? For example, say fetch took a string parameters representing the URL.

You can use closure, check example for this or docs.

let var = 42;
let f = || Ok::<u32, anyhow::Error>(var);
let result = f.retry(&ExponentialBuilder::default()).call()?;
yonas commented

@balroggg If fetch was the name of the function, how would we adapt our example to call it?

@balroggg If fetch was the name of the function, how would we adapt our example to call it?

sorry, we can't. We need to do something like:

let f = || async { fetch(a, b).await }

It's worth to take a look over https://github.com/apache/incubator-opendal/blob/main/src/layers/retry.rs

yonas commented

Alright, here's the code I'm trying to make compile:

src/main.rs

use anyhow::Result;
use backon::{ExponentialBuilder, Retryable};
use clap::Parser;

async fn fetch(url: String) -> Result<String> {
    Ok(reqwest::get(url).await?.text().await?)
}

/// URL to download
#[derive(Parser)]
#[clap(author="Name Here", version, about="Retriable HTTP downloader")]
struct Cli {
    /// The URL to download
    url: String,
}

#[tokio::main]
async fn main() -> Result<()> {
    let args = Cli::parse();
    //let url = std::env::args().nth(1).expect("no URL given");;
    let f = || async { fetch(args.url).await };
    let content = f.retry(&ExponentialBuilder::default()).await?;

    println!("fetch succeeded: {content}");

    Ok(())
}

Cargo.toml

[package]
name = "backon"
version = "0.1.0"
edition = "2021"

[dependencies]
reqwest = { version = "*", features = ["rustls"] }
clap = { version = "*", features = ["derive"] }
anyhow = "*"
backon = "*"
tokio = { version = "*", features = ["macros", "rt-multi-thread"] }

Hi, by use args.url, you capture the owner ship, so this clouse can't be used as FnMut.

You can change in this way:

#[tokio::main]
async fn main() -> Result<()> {
    let args = Cli::parse();
    //let url = std::env::args().nth(1).expect("no URL given");;

    let url = args.url;
    let content = {
        || async {
            let url = url.clone();
            fetch(url).await
        }
    }
    .retry(&ExponentialBuilder::default())
    .await?;

    println!("fetch succeeded: {content}");

    Ok(())
}
yonas commented

@Xuanwo Thanks that worked!

Would it be possible to have a function returning a function (something like currying) instead of using a closure to send arguments to what backon retries?

Would it be possible to have a function returning a function (something like currying) instead of using a closure to send arguments to what backon retries?

I'm not sure about that. Would you like to demonstrate that?

By reading this discussion about currying in Rust: https://internals.rust-lang.org/t/currying-in-rust/10326

I thought it would be possible to make backon works with this kind of function:

fn retryable_function_with_args(x: usize) -> impl Fn()->Result<usize, ()> {
    move || Ok(x)
}

Playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=6dd71348f66e1c7fc07340423e273b7f

But maybe it isn't?

I thought it would be possible to make backon works with this kind of function:

Thanks for the quick example. But I still don't understand how to use currying to send arguments for backon to retry. Can you give an example that more close to backon that how user will use it?

We can reuse this example for demo:

#[tokio::main]
async fn main() -> Result<()> {
    let args = Cli::parse();
    //let url = std::env::args().nth(1).expect("no URL given");;

    let url = args.url;
    let content = {
        || async {
            let url = url.clone();
            fetch(url).await
        }
    }
    .retry(&ExponentialBuilder::default())
    .await?;

    println!("fetch succeeded: {content}");

    Ok(())
}

Are you talking about replace the following part to a function?

{
    || async {
        let url = url.clone();
        fetch(url).await
    }
}

Are you talking about replace the following part to a function?

Yes, I am.

I came to this issue because I was looking for an example of "how to use backon with a reusable function that can slightly vary using arguments".

Because I think that:

async fn do_fetch(url: &str) -> Result<(), ()> {
    fetch(url)
}

#[tokio::main]
async fn main() -> Result<()> {
    // call to example.com
    do_fetch("https://example.com")
    .retry(&ExponentialBuilder::default())
    .await?;

    // call to google.com
    do_fetch("https://google.com")
    .retry(&ExponentialBuilder::default())
    .await?;

    Ok(())
}

would have been easier to maintain than duplicating the body of the function into 2 different closures:

#[tokio::main]
async fn main() -> Result<()> {
    // call to example.com
    let do_fetch = || fetch("https://example.com");
    do_fetch
    .retry(&ExponentialBuilder::default())
    .await?;

    // call to google.com
    let do_fetch = || fetch("https://google.com");
    do_fetch
    .retry(&ExponentialBuilder::default())
    .await?;

    Ok(())
}

Okay, I understand. Returning a function to generate a future is complex in rust.

  • We can't use impl FnMut() -> impl Future<Output=Result<()>> for now, so we have to return a box future.
  • We need to treat lifetime carefully.

The most close implementation will be:

async fn fetch(url: &str) -> Result<()> {
    Ok(())
}

fn do_fetch(url: &str) -> impl FnMut() -> BoxFuture<'static, Result<()>> + '_ {
    || {
        let url = url.to_string();
        Box::pin(async move { fetch(&url).await })
    }
}

#[tokio::main]
async fn main() -> Result<()> {
    // call to example.com
    do_fetch("abc")
        .retry(&ExponentialBuilder::default())
        .await?;

    // call to google.com
    do_fetch("def")
        .retry(&ExponentialBuilder::default())
        .await?;

    Ok(())
}

Playground link: https://www.rustexplorer.com/b/sgyqu4

Nice, thank you for your time.

I filled your playground to include the 4 use cases (blocking with owned args, blocking with borrowed args, async with owned args and async with borrowed args), so the readers of this issue should be able to simply pick the one that he/she needs... And I also added some randomness and logs to proves that backon really retries those functions.

/*
[dependencies]
backon = "*"
tokio = { version = "*", features = ["full"] }
anyhow = "*"
futures = "*"
rand = "*"
*/

use anyhow::{anyhow, Result};
use backon::ExponentialBuilder;
use backon::{BlockingRetryable, Retryable};
use futures::future::BoxFuture;

fn blocking_fetch_with_owned_args(url: String) -> Result<()> {
    if rand::random() {
        println!("success for {url}");
        Ok(())
    } else {
        println!("failed for {url}");
        Err(anyhow!("oops"))
    }
}
fn blocking_retryable_function_with_owned_args(url: String) -> impl Fn() -> Result<()> {
    move || {
        let url = url.clone();
        println!("[blocking_retryable_function_with_owned_args] called with {url}");
        blocking_fetch_with_owned_args(url)
    }
}

fn blocking_fetch_with_borrowed_args(url: &str) -> Result<()> {
    if rand::random() {
        println!("success for {url}");
        Ok(())
    } else {
        println!("failed for {url}");
        Err(anyhow!("oops"))
    }
}
fn blocking_retryable_function_with_borrowed_args(url: &str) -> impl Fn() -> Result<()> + '_ {
    move || {
        let url = url.to_string();
        println!("[blocking_retryable_function_with_borrowed_args] called with {url}");
        blocking_fetch_with_borrowed_args(&url)
    }
}

async fn async_fetch_with_owned_args(url: String) -> Result<()> {
    if rand::random() {
        println!("success for {url}");
        Ok(())
    } else {
        println!("failed for {url}");
        Err(anyhow!("oops"))
    }
}
fn async_retryable_function_with_owned_args(
    url: String,
) -> impl Fn() -> BoxFuture<'static, Result<()>> {
    move || {
        let url = url.clone();
        println!("[async_retryable_function_with_owned_args] called with {url}");
        Box::pin(async move { async_fetch_with_owned_args(url).await })
    }
}

async fn async_fetch_with_borrowed_args(url: &str) -> Result<()> {
    if rand::random() {
        println!("success for {url}");
        Ok(())
    } else {
        println!("failed for {url}");
        Err(anyhow!("oops"))
    }
}
fn async_retryable_function_with_borrowed_args(
    url: &str,
) -> impl Fn() -> BoxFuture<'static, Result<()>> + '_ {
    move || {
        let url = url.to_string();
        println!("[async_retryable_function_with_borrowed_args] called with {url}");
        Box::pin(async move { async_fetch_with_borrowed_args(&url).await })
    }
}

#[tokio::main]
async fn main() -> Result<()> {
    // blocking with owned args
    blocking_retryable_function_with_owned_args("blocking + owned args 1".to_string())
        .retry(&ExponentialBuilder::default())
        .call()?;
    blocking_retryable_function_with_owned_args("blocking + owned args 2".to_string())
        .retry(&ExponentialBuilder::default())
        .call()?;

    // blocking with borrowed args
    blocking_retryable_function_with_borrowed_args("blocking + borrowed args 1")
        .retry(&ExponentialBuilder::default())
        .call()?;
    blocking_retryable_function_with_borrowed_args("blocking + borrowed args 2")
        .retry(&ExponentialBuilder::default())
        .call()?;

    // async with owned args
    async_retryable_function_with_owned_args("async + owned args 1".to_string())
        .retry(&ExponentialBuilder::default())
        .await?;
    async_retryable_function_with_owned_args("async + owned args 2".to_string())
        .retry(&ExponentialBuilder::default())
        .await?;

    // async with borrowed args
    async_retryable_function_with_borrowed_args("async + borrowed args 1")
        .retry(&ExponentialBuilder::default())
        .await?;
    async_retryable_function_with_borrowed_args("async + borrowed args 2")
        .retry(&ExponentialBuilder::default())
        .await?;

    Ok(())
}

Playground link: https://www.rustexplorer.com/b/iortih

Should we add examples in https://github.com/Xuanwo/backon/tree/main/examples to better document those use cases?