Hi! I want to write a simple function that sends 6 (six) http requests to different servers with request timeout of 5 seconds (in case a server is down). I may get a positive result (key found) or a negative one (key not found). My goal is to return from the function as soon as I receive a positive result. Otherwise I return with a negative result. The simplest approach is to send requests in a sequential order one by one and check responses. However it may take much time. Instead, I'd like to create 6 async requests and wait until one of them returns with a positive response, and I return from the function (by canceling the rest of requests?).
I have searched for crates (thread-pool, etc), I also considered tokio, reqwest crates, however I am still confused about which approach would be best. Have you had similar a problem, or do you have any idea how I could go about it?
The general idea of “take the first one to complete” is called “select” in the async world; in your case since they’re all the same and they might fail, futures::future::select_ok() would be appropriate. Something like this:
I have implemented using tokio crate, it works as expected. The following code is just an example, may be useful for others who face the same question.
use reqwest::{Client, Response};
#[tokio::main]
async fn main() {
let output = tokio::select! {
val1 = reqwest1() => val1,
val2 = reqwest2() => val2,
val3 = block() => val3,
val4 = block() => val4
};
println!("{:?}", output.unwrap());
}
async fn reqwest1() -> Result<Response, reqwest::Error> {
let client = Client::new();
let body = "{\"x\": 10,\"y\": 5}";
client.post("http://localhost:3000/sub")
.header("Content-Type", "application/json")
.body(body)
.send().await
}
async fn reqwest2() -> Result<Response, reqwest::Error>{
let client = Client::new();
let body = "{\"x\": 5,\"y\": 3}";
client.post("http://localhost:3000/mul")
.header("Content-Type", "application/json")
.body(body)
.send().await
}
async fn block()-> Result<Response, reqwest::Error> {
let client = Client::new();
// the server simply sleeps for 30 seconds
client.post("http://localhost:3000/block")
.header("Content-Type", "application/json")
.send().await
}
If it's only six requests then using threads would also do the job. It only takes microseconds to start a thread while HTTP requests usually take milliseconds to seconds.
Though if you want to scale this to doing those six requests a thousand times per second then using an async runtime would be a better choice because the thread overhead would become significant.
I understand your point. But how would you implement the logic of "return as=s soon as you get the first positive response (successful search in db)"? I don't want to wait for all 6 responses, just the first one with a positive answer. Though my solution does not do this either, it does not check the result whether it is positive or not. The only solution comes to my mind is communication via channels.
If my operations are cpu-heavy computations which take place inside an async function, then cancelation makes sense, but what about a simple request over network to a remote server with timeout of 6 seconds? How are you going to cancel the request which is already sent to a remote server? We simply ignore other responses if we already get a successful one.
Or do you suggest to cancel the process of waiting for a response (from a remote server)? I have no idea how to cancel the following axum request:
None of that seems necessary for OP's request, letting already-sent queries run to completion and implicitly discarding their results should work just fine.
Cancelation means the TCP connection is torn down or the HTTP/2 connection sends a RST_STREAM. You trigger it by Dropping the Future, which your tokio::select! code will do automatically!