Running an actix web server on a separate thread
Asked Answered
T

1

5

I'm new to actix, and I'm trying to understand how I can run a server on one thread and send requests from another.

This is the code I have so far

use actix_web::{web, App, HttpResponse, HttpServer};
use std::{sync::mpsc::channel, thread};

#[actix_web::main]
async fn main() {
    let (tx, rx) = channel();
    thread::spawn(move || {
        let srv =
            HttpServer::new(|| App::new().default_service(web::to(|| HttpResponse::NotFound())))
                .bind("localhost:12347")
                .unwrap()
                .run();
        let _ = tx.send(srv);
    });
    reqwest::get("http://localhost:12347").await.unwrap();
    let srv = rx.recv().unwrap();
    srv.handle().stop(false).await;
}

It compiles just fine, but it gets stuck on on sending the request. It seems like the server is running, soI can't figure out why I am not getting a response.

EDIT: As suggested by @Finomnis and @cafce25,I changed the code to use tasks instead of threads, and awaited te result of .run()

use actix_web::{web, App, HttpResponse, HttpServer};
use std::{sync::mpsc::channel, thread};

#[actix_web::main]
async fn main() {
    let (tx, rx) = channel();
    tokio::spawn(async move {
        let srv =
            HttpServer::new(|| App::new().default_service(web::to(|| HttpResponse::NotFound())))
                .bind("localhost:12347")
                .unwrap()
                .run();
        let _ = tx.send(srv.handle());
        srv.await.unwrap();
    });
    reqwest::get("http://localhost:12347").await.unwrap();
    let handle = rx.recv().unwrap();
    handle.stop(false).await;
}

which solves the problem. I'm still curious if it is possible to do it on different threads since I can't use await inside a synchronous function.

Torres answered 8/2, 2023 at 13:26 Comment(12)
Why on a different thread? Threading and async programming usually shouldn't be mixed (until a deep understanding of how async works is reached), instead spawn a new task.Bitch
The server would be very poorly implemented if it stopped running after the first request handled.Hydrodynamic
@Hydrodynamic What do you mean with that?Bitch
Either you can handle requests in the thread and it never returns or the requwest::get(...) deadlocks cause the server doesn't handle requests yet. Or in other words they should follow your first suggestion @Finomnis.Hydrodynamic
@Bitch Changing thread to tokio (and slightly modifying the closure) still gives the same behaviour. So I don't think this is the source of the problemTorres
@Hydrodynamic I'm not sure I understand your feedback. Doesn't the server handle requests by returning a 404?Torres
Yes but it doesn't return it to your rust code but to the request made. Also you have to .await the results of .run() for any listening to happen.Hydrodynamic
@Hydrodynamic Thank you for all the feedback. Both examples should be syntactically correct now. I didn't post it as an answer since it doesn't solve my original question, which is how to achieve this on different threadsTorres
What's the actual problem you're trying to solve? Because for all you know the runtime might as well put the server on a different thread from the request.Hydrodynamic
You should not use synchronous channels in asynchronous code (although I don't know if that's your problem).Pinfold
"which is how to achieve this on different threads" - you can't and shouldn't - the HttpServer itself already does multithreading internally. Moving it onto a different thread will give you no performance benefit. As mentioned earlier, move it to a task instead. Not on a tokio task, though - #[actix_web::main] has its own runtime. You need to spawn an actix-web task instead. Of course you can use the #[tokio::main] runtime instead if you want to spawn tokio tasks. actix-web is compatible with that.Bitch
Read the documentation of run(): This method starts a number of HTTP workers in separate threads.. You don't need to put it on a thread yourself, just spawn the result of run(). And no, you cannot go from asynchronous to synchronous and back to asynchronous. Once you are in synchronous land, you have no good way of returning back to async.Bitch
B
7

There are a couple of things wrong with your code; the biggest one being that you never .await the run() method.

For that fact alone you cannot run it in a normal thread, it has to exist in an async task.

So what happens is:

  • you create the server
  • the server never runs because it doesn't get awaited
  • you query the server for a response
  • the response never comes because the server doesn't run, so you get stuck in reqwest::get

What you should do instead:

  • start the server.

Also:

  • You don't need to propagate the server object out to stop it. You can create a .handle() first before you move it into the task. The server handle does not contain a reference to the server, it's based on smart pointers instead.
  • NEVER use synchronous channels with async tasks. It will block the runtime, dead-locking everything. (The only reason it worked in your second example is because it is most likely a multi-threaded runtime and you only dead-locked one of the runtime cores. Still bad.)
  • (Maybe) don't tokio::spawn if you use #[actix_web::main]. actix-web has its own runtime, you need to actix_web::rt::spawn with it. If you want to use tokio based tasks, you need to do #[tokio::main]. actix-web is compatible with the tokio runtime. (EDIT: actix-web might be compatible with tokio::spawn(), I just didn't find documentation anywhere that says it is)

With all that fixed, here is a working version:

use actix_web::{rt, web, App, HttpResponse, HttpServer};

#[actix_web::main]
async fn main() {
    let srv = HttpServer::new(|| App::new().default_service(web::to(|| HttpResponse::NotFound())))
        .bind("localhost:12347")
        .unwrap()
        .run();
    let srv_handle = srv.handle();

    rt::spawn(srv);

    let response = reqwest::get("http://localhost:12347").await.unwrap();
    println!("Response code: {:?}", response.status());

    srv_handle.stop(false).await;
}
Response code: 404
Bitch answered 8/2, 2023 at 14:56 Comment(3)
I think actix is compatible with tokio::spawn().Pinfold
Thank you for the detailed answer! This really helped things "click" for me :)Torres
@ChayimFriedman It seems like it, but it isn't specified anywhere to my knowledge. And as actix provides its own spawn method, I think it's better to use that one. But of course it might be compatible, so I really don't know.Bitch

© 2022 - 2024 — McMap. All rights reserved.