raf
raf

Reputation: 52

Call a random function with variable arguments dynamically

I have a list of functions with variable arguments, and I want to randomly pick one of them, in runtime, and call it, on a loop. I'm looking to enhance the performance of my solution.

I have a function that calculates the arguments based on some randomness, and then (should) return a function pointer, which I could then call.

pub async fn choose_random_endpoint(
    &self,
    rng: ThreadRng,
    endpoint_type: EndpointType,
) -> impl Future<Output = Result<std::string::String, MyError>> {
    match endpoint_type {
        EndpointType::Type1 => {
            let endpoint_arguments = self.choose_endpoint_arguments(rng);
            let endpoint = endpoint1(&self.arg1, &self.arg2, &endpoint_arguments.arg3);
            endpoint
        }
        EndpointType::Type2 => {
            let endpoint_arguments = self.choose_endpoint_arguments(rng);
            let endpoint = endpoint2(
                &self.arg1,
                &self.arg2,
                &endpoint_arguments.arg3,
                rng.clone(),
            );
            endpoint
        }
        EndpointType::Type3 => {
            let endpoint_arguments = self.choose_endpoint_arguments(rng);
            let endpoint = endpoint3(
                &self.arg1,
                &self.arg2,
                &endpoint_arguments.arg3,
                rng.clone(),
            );
            endpoint
        }
    }
}

The error I obtain is

expected opaque type `impl Future<Output = Result<std::string::String, MyError>>` (opaque type at <src/calls/type1.rs:14:6>)
   found opaque type `impl Future<Output = Result<std::string::String, MyError>>` (opaque type at <src/type2.rs:19:6>)

. The compiler advises me to await the endpoints, and this solves the issue, but is there a performance overhead to this?

Outer function:

Aassume there is a loop calling this function:

pub async fn make_call(arg1: &str, arg2: &str) -> Result<String> {
    let mut rng = rand::thread_rng();
    let random_endpoint_type = choose_random_endpoint_type(&mut rng);
    let random_endpoint = choose_random_endpoint(&rng, random_endpoint_type);
    // call the endpoint
    Ok(response)
}

Now, I want to call make_call every X seconds, but I don't want my main thread to block during the endpoint calls, as those are expensive. I suppose the right way to approach this is spawning a new thread per X seconds of interval, that call make_call?

Also, performance-wise: having so many clones on the rng seems quite expensive. Is there a more performant way to do this?

Upvotes: 1

Views: 122

Answers (1)

drewtato
drewtato

Reputation: 12777

The error you get is sort of unrelated to async. It's the same one you get when you try to return two different iterators from a function. Your function as written doesn't even need to be async. I'm going to remove async from it when it's not needed, but if you need async (like for implementing an async-trait) then you can add it back and it'll probably work the same.

I've reduced your code into a simpler example that has the same issue (playground):

async fn a() -> &'static str {
    "a"
}

async fn b() -> &'static str {
    "b"
}

fn a_or_b() -> impl Future<Output = &'static str> {
    if rand::random() {
        a()
    } else {
        b()
    }
}

What you're trying to write

When you want to return a trait, but the specific type that implements that trait isn't known at compile time, you can return a trait object. Futures need to be Unpin to be awaited, so this uses a pinned box (playground).

fn a_or_b() -> Pin<Box<dyn Future<Output = &'static str>>> {
    if rand::random() {
        Box::pin(a())
    } else {
        Box::pin(b())
    }
}

You may need the type to be something like Pin<Box<dyn Future<Output = &'static str> + Send + Sync + 'static>> depending on the context.

What you should write

I think the only reason you'd do the above is if you want to generate the future with some kind of async rng, then do something else, and then run the generated future after that. Otherwise there's no need to have nested futures; just await the inner futures when you call them (playground).

async fn a_or_b() -> &'static str {
    if rand::random() {
        a().await
    } else {
        b().await
    }
}

This is conceptually equivalent to the Pin<Box> method, just without having to allocate a Box. Instead, you have an opaque type that implements Future itself.

Blocking

The blocking behavior of these is only slightly different. Pin<Box> will block on non-async things when you call it, while the async one will block on non-async things where you await it. This is probably mostly the random generation.

The blocking behavior of the endpoint is the same and depends on what happens inside there. It'll block or not block wherever you await either way.

If you want to have multiple make_call calls happening at the same time, you'll need to do that outside the function anyway. Using the tokio runtime, it would look something like this:

use tokio::task;
use futures::future::join_all;
let tasks: Vec<_> = (0..100).map(|_| task::spawn(make_call())).collect();
let results = join_all(tasks).await;

This also lets you do other stuff while the futures are running, in between collect(); and let results.

If something inside your function blocks, you'd want to spawn it with task::spawn_blocking (and then await that handle) so that the await call in make_call doesn't get blocked.

RNG

If your runtime is multithreaded, the ThreadRng will be an issue. You could create a type that implements Rng + Send with from_entropy, and pass that into your functions. Or you can call thread_rng or even just rand::random where you need it. This makes a new rng per thread, but will reuse them on later calls since it's a thread-local static. On the other hand, if you don't need as much randomness, you can go with a Rng + Send type from the beginning.

If your runtime isn't multithreaded, you should be able to pass &mut ThreadRng all the way through, assuming the borrow checker is smart enough. You won't be able to pass it into an async function and then spawn it, though, so you'd have to create a new one inside that function.

Upvotes: 1

Related Questions