2

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.

14159
  • 125
  • 5
  • 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. – Finomnis Feb 08 '23 at 13:35
  • The server would be very poorly implemented if it stopped running after the first request handled. – cafce25 Feb 08 '23 at 13:39
  • @cafce25 What do you mean with that? – Finomnis Feb 08 '23 at 13:40
  • 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. – cafce25 Feb 08 '23 at 13:42
  • @Finomnis 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 problem – 14159 Feb 08 '23 at 13:43
  • @cafce25 I'm not sure I understand your feedback. Doesn't the server handle requests by returning a 404? – 14159 Feb 08 '23 at 13:46
  • 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. – cafce25 Feb 08 '23 at 13:51
  • @cafce25 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 threads – 14159 Feb 08 '23 at 14:18
  • 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. – cafce25 Feb 08 '23 at 14:21
  • You should not use synchronous channels in asynchronous code (although I don't know if that's your problem). – Chayim Friedman Feb 08 '23 at 14:49
  • *"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. – Finomnis Feb 08 '23 at 14:59
  • 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. – Finomnis Feb 08 '23 at 15:19

1 Answers1

3

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
Finomnis
  • 18,094
  • 1
  • 20
  • 27
  • I think actix is compatible with `tokio::spawn()`. – Chayim Friedman Feb 08 '23 at 15:41
  • Thank you for the detailed answer! This really helped things "click" for me :) – 14159 Feb 08 '23 at 16:00
  • @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. – Finomnis Feb 08 '23 at 16:15