How to return hyper::Server or at least axum::Route from function

Hello!

I need to make a single application that embeds 2 web servers (different ports, different endpoints, different handlers) plus some glue logic.

I managed to put this together in a flat file (compiles and works as expected):

// [package]
// name = "multi_server"
// version = "1.0.0"
// edition = "2018"
//
// [dependencies]
// axum = "0.1"
// tokio = { version = "1.9", features = ["full"] }
// hyper = { version = "0.14", features = ["full"] }

use axum::prelude::*;
use hyper;

async fn root1() -> &'static str {
    "Hello, World from server 1!"
}

async fn root2() -> &'static str {
    "Hello, World from server 2!"
}

#[tokio::main]
async fn main() -> Result<(), hyper::Error> {
    let app1 = route("/", get(root1));

    let server1 =
        hyper::Server::bind(&"0.0.0.0:3000".parse().unwrap()).serve(app1.into_make_service());

    let app2 = route("/", get(root2));

    let server2 =
        hyper::Server::bind(&"0.0.0.0:3001".parse().unwrap()).serve(app2.into_make_service());

    let (r1, r2) = tokio::join!(server1, server2);

    r1.and(r2).and(Ok(()))
}

Thing I'm trying and I lost most of today is: how to put each server in a submodule and build them with module-local function:

// [package]
// name = "multi_server"
// version = "1.0.0"
// edition = "2018"
//
// [dependencies]
// axum = "0.1"
// tokio = { version = "1.9", features = ["full"] }
// hyper = { version = "0.14", features = ["full"] }

use axum::prelude::*;
use hyper;

mod s1 {
    async fn root() -> &'static str {
        "Hello, World from server 1!"
    }

    pub async fn build(addr: &str) -> WTF_PUT_HERE {
        use axum::prelude::*;
        let app = route("/", get(root));
        hyper::Server::bind(&addr.parse().unwrap()).serve(app.into_make_service())
    }

    // OR

    pub async fn app() -> WTF_PUT_HERE {
        use axum::prelude::*;
        route("/", get(root))
    }
}

mod s2 {
    async fn root() -> &'static str {
        "Hello, World from server 2!"
    }

    pub async fn build(addr: &str) -> WTF_PUT_HERE {
        use axum::prelude::*;
        let app = route("/", get(root));
        hyper::Server::bind(&addr.parse().unwrap()).serve(app.into_make_service())
    }

    // OR

    pub async fn app() -> WTF_PUT_HERE {
        use axum::prelude::*;
        route("/", get(root))
    }
}

#[tokio::main]
async fn main() -> Result<(), hyper::Error> {
    let server1 = s1::build("0.0.0.0:3000");
    let server2 = s1::build("0.0.0.0:3001");

    // OR (at least)

    let server1 = hyper::Server::bind("0.0.0.0:3000".parse().unwrap()).serve(s1::app().into_make_service());
    let server2 = hyper::Server::bind("0.0.0.0:3001".parse().unwrap()).serve(s2::app().into_make_service());

    let (r1, r2) = tokio::join!(server1, server2);

    r1.and(r2).and(Ok(()))
}

And yes, I tried putting in the WTF_PUT_HERE monstertypes compiler suggested, but each attempt ended with compiler telling to finger myself.

I browsed for hours through all forums, lists, docs, manuals, howtos, examples and sources. Zero. Nada. Null. References to nonexisting documents. References to outdated packages. References to alpha drafts. Metoos. References to chapters consisting of a sole TBD. Overall documentation on anything async is unfortunately worthless (sorry about that, but that's what I feel after today). It gives you a concrete example (working, after you spend good time with your crystal ball deducting the correct features in [dependencies]) and absolutely no clue how to sidestep even a micron.

I'm so damn close to slamming the door and going back to golang despite all my love for Rust all in itself.

Try this:

mod s1 {
    use std::future::Future;

    async fn root() -> &'static str {
        "Hello, World from server 1!"
    }

    pub fn build(addr: &str) -> impl Future<Output = hyper::Result<()>> {
        use axum::prelude::*;
        let app = route("/", get(root));
        hyper::Server::bind(&addr.parse().unwrap()).serve(app.into_make_service())
    }
}

mod s2 {
    use std::future::Future;

    async fn root() -> &'static str {
        "Hello, World from server 2!"
    }

    pub fn build(addr: &str) -> impl Future<Output = hyper::Result<()>> {
        use axum::prelude::*;
        let app = route("/", get(root));
        hyper::Server::bind(&addr.parse().unwrap()).serve(app.into_make_service())
    }
}

#[tokio::main]
async fn main() -> Result<(), hyper::Error> {
    let server1 = s1::build("0.0.0.0:3000");
    let server2 = s2::build("0.0.0.0:3001");

    tokio::try_join!(server1, server2)?;

    Ok(())
}

Axum is a very new library, so it's natural that the documentation is incomplete. I know that new examples are being added every day at the moment.

2 Likes

Alternatively

    pub async fn build(addr: &str) -> hyper::Result<()> {
        use axum::prelude::*;
        let app = route("/", get(root));
        hyper::Server::bind(&addr.parse().unwrap())
            .serve(app.into_make_service())
            .await
    }

seems to work as well.

This won't run servers in parallel (as far as I browsed all the docs).

The solution that @Heliozoa posted is equivalent to mine. The try_join! will work in the same way for both.

You can return the route object like this:

mod s1 {
    use std::future::Future;
    use axum::prelude::*;

    async fn root() -> &'static str {
        "Hello, World from server 1!"
    }

    pub fn build(addr: &str) -> impl Future<Output = hyper::Result<()>> {
        let app = app();
        hyper::Server::bind(&addr.parse().unwrap()).serve(app.into_make_service())
    }

    pub fn app() -> axum::routing::BoxRoute<Body> {
        route("/", get(root)).boxed()
    }
}
1 Like

Though I would usually advocate for spawning rather than try_join in this situation. This would also ensure that both run at the same time.

Slightly off topic: I'm sorry you had such a bad experience. I've been there myself and know how frustrating it can be.

I'm the author of axum and if you have specific suggestions for improving our docs that would be great. I really wanna make sure axum has great documentation. We can chat in the axum channel in the tokio discord Tokio if you want.

1 Like

Thank you for both fixes (build and app). That's the kind of information and example that's notoriously missing from docs. Staring literally for hours on function signatures didn't bring me a step closer to that solution... :frowning:

I'll try to look at spawning (all by myself at first :slight_smile:). Recently bought a book that tries to touch that subject slightly more in-depth (Rust for Rustaceans by Jon Gjengset, a great book TBH).

The problem was more on Hyper-side of documentation. What I work on now is something like this:
one server listening on external interface for client connections and talking to them over WebSocks. Clients can issue logs to be pushed to DB and metrics to be pushed to another DB. The second server on internal interface accepts REST PUT with JSON and dispatches it to relevant clients on the other side. Sort of message broker with more elaborated business logic and somehow asymmetric flow.

I'll gladly share my experience along the way, both good and bad.

As with do's and dont's, what I understood:

let r1 = a1.await
let r2 = a2.await

waits for a1 completion before launching a2, while

let (r1,r2) = tokio::join!(a1,a2)

runs a1 and a2 in parallel.

Or I misunderstood things miserably again - I base it on Async-Book/join?

Yes, that's exactly right. When you type .await, that will make your program wait until the awaited thing has finished before it continues.

On the other hand, when you use tokio::join!, this macro will internally insert the .awaits in the right way so both start running at the same time.

Similarly, to simulate the tokio::join! with spawning, you do this:

let task1 = tokio::spawn(a1);
let task2 = tokio::spawn(a2);
task1.await;
task2.await;

This works because the tokio::spawn method makes things start running immediately, so task2 is already running in the background even though you haven't gotten past task1.await yet.

1 Like

Soooo... The solution by @Heliozoa seems to me like the first case - build() with trailing .await blocks until the server terminates and only then calls the other build().

Right, but that's only if you also await the build() call immediately. The contents of the build() call do not start running until the future returned by build() is awaited, spawned, given to try_join! or otherwise polled.

pub async fn build(addr: &str) -> hyper::Result<()> {
        use axum::prelude::*;
        let app = route("/", get(root));
        hyper::Server::bind(&addr.parse().unwrap())
            .serve(app.into_make_service())
            .await  // <-- Won't that block?
    }

Ok, I won't waste your precious time more today. I must sleep it over.

Thank you for your invaluable and in-depth help.

Yes, but only internally in the function. Consider the main function again:

#[tokio::main]
async fn main() -> Result<(), hyper::Error> {
    let server1 = s1::build("0.0.0.0:3000");
    let server2 = s2::build("0.0.0.0:3001");
    // The contents of server1 or server2 have not yet started running.

    // Now we start server1 and server2 at the same time.
    tokio::try_join!(server1, server2)?;

    Ok(())
}

This example may help illustrate the point:

use tokio::time::Duration;

async fn sleep_then_print(timer: i32) {
    println!("Start timer {}.", timer);

    tokio::time::sleep(Duration::from_secs(1)).await;

    println!("Timer {} done.", timer);
}

#[tokio::main]
async fn main() {
    // The join! macro lets you run multiple things concurrently.
    tokio::join!(
        sleep_then_print(1),
        sleep_then_print(2),
        sleep_then_print(3),
    );
}
Start timer 1.
Start timer 2.
Start timer 3.
Timer 1 done. <-- 1 second later
Timer 2 done.
Timer 3 done.

playground, article it was taken from

1 Like

O, that's my a-ha moment. That single try_run! line is the one, that actually starts everything ticking. Engage, Mr Sulu.

I assume the article you referenced is of your enlightened authorship. It should be linked straight from both async-book and tokio initial pages, as it explains much more in a much simpler and intuitive way. Thank you for sharing.

2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.