Awaiting async calls in actix-web

Hi all,

Just trying to get my head around async/.await in a small project and how best to manage them.

Using the latest actix-web 2.0 version that is async by default, I've got a main function that builds a server in this manner:

    HttpServer::new(move || {
        App::new()
            // ...
            .service(web::resource("/contact").route(web::post().to(process_contact_request)))
    })
    .bind("127.0.0.1:8000")?
    .run()
    .await

with a handler looking like

async fn process_contact_request(
    contact_request: web::Json<ContactRequest>,
    req: HttpRequest,
    kv: web::Data<KVStore>,
) -> Result<HttpResponse, Error> {
    // Do a bunch of checks
    // ...
    if *captcha == contact.challenge {
        // Logging, other processing
        // ...

        // Send off another async request to a separate server.
        report_contact(
            &contact,
            &referer,
            req.connection_info().remote().unwrap_or("Unknown"),
        )
        .await?;

        // Response to frontend
        Ok(HttpResponse::Ok().finish())
    } else {
        Ok(HttpResponse::BadRequest().body("Invalid challenge value"))
    }
}

What I'm struggling with here is how to treat this report_contact call.
Since it will take some time to process, calling .await right here is blocking the Ok response.

I'd like to be able to return the Ok and await the result afterward. In fact, since report_contact(...) -> Result<(), Error>, I'm only interested in error propagation/handling; there is no need to process some return value of this call.

My best guess would be to use a wrap_fn call for the web::resource handler and send off the report_contact request at the middleware level, but can't really figure out how to invoke it at that level, since I need to pass it a few processed variables as you can see.

Is this the right way forward, or have I missed something in the way one should handle async code correctly in this context?

This is true, but the point of the await here is that it is not blocking other client requests from calling process_contact_request.

If you want to immediately return a response to these requests, while running report_contact in the background, you can spawn the future with e.g. actix::spawn

Depending on what kind of work this function does, you may want to apply some constraints to it. For example, a CPU-heavy workload may not be ideal to run on the current arbiter. For more control, you can write an actor that is able to schedule these calls using your preferred scheduling mechanism. The Actix book does a good job explaining the concept of an actor, but it's up to you to implement it.

This might not be possible, depending on the specific implementation of report_contact. If the error is only returned prior to the first async yield point (an await internal to this function), then it may be possible to move the spawn inside the function. So that report_contract itself is no longer async, but it spawns a Future as an internal implementation detail.

Also keep in mind that if you need a return value from report_contact, then spawning the Future like this will not help you. For this situation, you should consider using a Stream response that can provide the HttpResponse::Ok or HttpResponse::BadRequest immediately. And then the additional return value from report_contact later when it becomes available. You can use BodyStream for this.

2 Likes

This is all great info, thanks for the clarifications!