Skip to content

Commit

Permalink
.await version of the simple_async_handlers example
Browse files Browse the repository at this point in the history
  • Loading branch information
alsuren committed Nov 16, 2019
1 parent a138306 commit 309c89f
Show file tree
Hide file tree
Showing 7 changed files with 116 additions and 74 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ members = [
"examples/handlers/request_data",
"examples/handlers/stateful",
"examples/handlers/simple_async_handlers",
"examples/handlers/simple_async_handlers_await",
"examples/handlers/async_handlers",
"examples/handlers/form_urlencoded",
"examples/handlers/multipart",
Expand Down
1 change: 1 addition & 0 deletions examples/handlers/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ We recommend reviewing our handler examples in the order shown below:
1. [Request Data](request_data) - Accessing common request information
1. [Stateful Handlers](stateful) - Keeping state in a handler
1. [Simple Async Handlers](simple_async_handlers) - Async Request Handlers 101
1. [Simple Async Handlers (.await version)](simple_async_handlers_await) - Request Handlers that use async/.await
1. [Async Handlers](async_handlers) - More complicated async request handlers

## Help
Expand Down
1 change: 1 addition & 0 deletions examples/handlers/simple_async_handlers/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ description = "An example that does asynchronous work before responding"
version = "0.0.0"
authors = ["David Laban <alsuren@gmail.com>"]
publish = false
edition = "2018"

[dependencies]
gotham = { path = "../../../gotham" }
Expand Down
3 changes: 3 additions & 0 deletions examples/handlers/simple_async_handlers/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ find yourself doing lots of CPU/memory intensive operations on the web server,
then futures are probably not going to help your performance, and you might be
better off spawning a new thread per request.

If you came here looking for an example that uses async/.await, please read
[Async Request Handlers (.await version)](../simple_async_handlers).

## Running

From the `examples/handlers/async_handlers` directory:
Expand Down
10 changes: 8 additions & 2 deletions examples/handlers/simple_async_handlers_await/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
[package]
name = "gotham_examples_handlers_simple_async_handlers"
name = "gotham_examples_handlers_simple_async_handlers_await"
description = "An example that does asynchronous work before responding"
version = "0.0.0"
authors = ["David Laban <alsuren@gmail.com>"]
publish = false
edition = "2018"

[dependencies]
gotham = { path = "../../../gotham" }
gotham_derive = { path = "../../../gotham_derive" }

hyper = "0.12"
mime = "0.3"
futures = "0.1"
# We need two versions of the futures library. One that matches what Gotham
# understands, and one that matches what .await understands. Stolen from:
# https://rust-lang-nursery.github.io/futures-rs/blog/2019/04/18/compatibility-layer.html
# (although they call the old version futures01 and we call it legacy_futures)
legacy_futures = {package = "futures", version = "0.1"}
futures = {package = "futures", version = "0.3.1", features = ["compat"]}
serde = "1.0"
serde_derive = "1.0"
tokio = "0.1"
31 changes: 9 additions & 22 deletions examples/handlers/simple_async_handlers_await/README.md
Original file line number Diff line number Diff line change
@@ -1,28 +1,15 @@
# Async Request Handlers
# Async Request Handlers (.await version)

The idea of async handlers has already been introduced by the post_handler example in
[Request Data](../request_data), which waits for the POST body asyncronously, and resolves
the response future once it has processed the body.

This example contains a pair of endpoints that sleep for a number of seconds,
in different ways. Note that we never call `std::thread::sleep` in this example,
so none of our request handlers block the thread from handling other requests
in parallel. Instead, in each case, we return a future, which will resolve when
the requested time has elapsed, and cause Gotham to respond to the http request.

The approach of using futures to track the status of long-running operations
is significantly lower overhead than that of spawning a new thread per request.
In our case, the long-running operations are sleeps, but in
[Async Handlers](../async_handlers), the long-running operations are http
requests.

You will often find that most of the time that a web server spends dealing
with a web request, it is waiting on another service (e.g. a database, or an
external api). If you can track these operations using futures, you should end
up with a very lightweight and performant web server. On the other hand, if you
find yourself doing lots of CPU/memory intensive operations on the web server,
then futures are probably not going to help your performance, and you might be
better off spawning a new thread per request.
the response future once it has processed the body. The combinator-based version
of this example can be found at [Async Request Handlers](../simple_async_handlers).

This example has exactly the same behavior and API as the combinator-based version,
and it can be used as a reference when converting your code to use async/await.
It also leaves the versions of gotham, tokio and hyper the same, and uses the
compatibility helpers from the `futures` crate to convert things at the
interface boundaries.

## Running

Expand Down
143 changes: 93 additions & 50 deletions examples/handlers/simple_async_handlers_await/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ extern crate serde;
extern crate serde_derive;
extern crate tokio;

use futures::{stream, Future, Stream};
use futures::compat::Future01CompatExt;
use futures::{FutureExt, TryFutureExt};
use legacy_futures::Future as LegacyFuture;

use std::time::{Duration, Instant};

use hyper::StatusCode;
Expand All @@ -25,7 +28,7 @@ use gotham::state::{FromState, State};

use tokio::timer::Delay;

type SleepFuture = Box<dyn Future<Item = Vec<u8>, Error = HandlerError> + Send>;
type SleepFuture = Box<dyn LegacyFuture<Item = Vec<u8>, Error = HandlerError> + Send>;

#[derive(Deserialize, StateData, StaticResponseExtender)]
struct QueryStringExtractor {
Expand Down Expand Up @@ -55,6 +58,14 @@ fn get_duration(seconds: u64) -> Duration {
/// web apis) can be coerced into returning futures that yield useful data,
/// so the patterns that you learn in this example should be applicable to
/// real world problems.
///
/// This function returns a LegacyFuture (a Future from the 0.1.x branch of
/// the `futures` crate rather than a std::future::Future that we can .await).
/// This is partly to keep it the same as the simple_async_handlers example,
/// and partly to show you how to use the .compat() combinators (because you
/// will probably be using them a lot while the ecosystem stabilises). For a
/// better explanation of .compat(), please read this blog post:
/// https://rust-lang-nursery.github.io/futures-rs/blog/2019/04/18/compatibility-layer.html
fn sleep(seconds: u64) -> SleepFuture {
let when = Instant::now() + get_duration(seconds);
let delay = Delay::new(when)
Expand All @@ -71,60 +82,92 @@ fn sleep(seconds: u64) -> SleepFuture {
/// This handler sleeps for the requested number of seconds, using the `sleep()`
/// helper method, above.
fn sleep_handler(mut state: State) -> Box<HandlerFuture> {
let seconds = QueryStringExtractor::take_from(&mut state).seconds;
println!("sleep for {} seconds once: starting", seconds);

// Here, we call our helper function that returns a future.
let sleep_future = sleep(seconds);

// Here, we convert the future from `sleep()` into the form that Gotham expects.
// We have to use .then() rather than .and_then() because we need to coerce both
// the success and error cases into the right shape.
// `state` is moved in, so that we can return it, and we convert any errors
// that we have into the form that Hyper expects, using the helper from
// IntoHandlerError.
Box::new(sleep_future.then(move |result| match result {
Ok(data) => {
let res = create_response(&state, StatusCode::OK, mime::TEXT_PLAIN, data);
println!("sleep for {} seconds once: finished", seconds);
Ok((state, res))
let async_block_future = async move {
let seconds = QueryStringExtractor::take_from(&mut state).seconds;
println!("sleep for {} seconds once: starting", seconds);
// Here, we call the sleep function and turn its old-style future into
// a new-style future. Note that this step doesn't block: it just sets
// up the timer so that we can use it later.
let sleep_future = sleep(seconds).compat();

// Here is where the serious sleeping happens. We yield execution of
// this block until sleep_future is resolved.
// The Ok("slept for x seconds") value is stored in result.
let result = sleep_future.await;

// Here, we convert the result from `sleep()` into the form that Gotham
// expects: `state` is owned by this block so we need to return it.
// We also convert any errors that we have into the form that Hyper
// expects, using the helper from IntoHandlerError.
match result {
Ok(data) => {
let res = create_response(&state, StatusCode::OK, mime::TEXT_PLAIN, data);
println!("sleep for {} seconds once: finished", seconds);
Ok((state, res))
}
Err(err) => Err((state, err.into_handler_error())),
}
Err(err) => Err((state, err.into_handler_error())),
}))
};
// Here, we convert the new-style future produced by the async block into
// an old-style future that gotham can understand. There are a couple of
// layers of boxes, which is a bit sad, but these will go away once the
// ecosystem settles and we can return std::future::Future from Handler
// functions. Think of it as a temporary wart.
Box::new(async_block_future.boxed().compat())
}

/// This example uses a `future::Stream` to implement a `for` loop. It calls sleep(1)
/// as many times as needed to make the requested duration.
/// It calls sleep(1) as many times as needed to make the requested duration.
///
/// https://github.com/alexcrichton/futures-await has a more readable syntax for
/// async for loops, if you are using nightly Rust.
/// Notice how much easier it is to read than the version in
/// `simple_async_handlers`.
fn loop_handler(mut state: State) -> Box<HandlerFuture> {
let seconds = QueryStringExtractor::take_from(&mut state).seconds;
println!("sleep for one second {} times: starting", seconds);

// Here, we create a stream of Ok(_) that's as long as we need, and use fold
// to loop over it asyncronously, accumulating the return values from sleep().
let sleep_future: SleepFuture = Box::new(stream::iter_ok(0..seconds).fold(
Vec::new(),
move |mut accumulator, _| {
// Do the sleep(), and append the result to the accumulator so that it can
// be returned.
sleep(1).and_then(move |body| {
accumulator.extend(body);
Ok(accumulator)
})
},
));

// This bit is the same as the bit in the first example.
Box::new(sleep_future.then(move |result| match result {
Ok(data) => {
let res = create_response(&state, StatusCode::OK, mime::TEXT_PLAIN, data);
println!("sleep for one second {} times: finished", seconds);
Ok((state, res))
let async_block_future = async move {
let seconds = QueryStringExtractor::take_from(&mut state).seconds;
println!("sleep for one second {} times: starting", seconds);

// We can't use the ? operator in the outermost async block, because we
// need to need to return ownership of the State object back to gotham.
// I quite like using ?, so I often find myself writing `async {}.await`
// to get around this problem when I'm feeling lazy. Think of this
// self-awaiting-block as a bit like a try block.
//
// In real code, you probably shouldn't be writing business logic in
// your Handler functions anyway. Instead, you should be
// unpacking everything you need from State in the Handler function
// and then calling your business logic with only the dependencies that
// they need. That way your business logic can use new-style futures
// and ? as much as it likes, and you will only need to update your
// handler functions (which don't contain any business logic) when you
// upgrade your gotham.
let result = async {
// The code within this block reads exactly like syncronous code.
// This is the style that you should aim to write your business
// logic in.
let mut accumulator = Vec::new();
for _ in 0..seconds {
let body = sleep(1).compat().await?;
accumulator.extend(body)
}
// ? does type coercion for us, so we need to use a turbofish to
// tell the compiler that we have a HandlerError. See this section
// of the rust async book for more details:
// https://rust-lang.github.io/async-book/07_workarounds/03_err_in_async_blocks.html
Ok::<_, HandlerError>(accumulator)
}
.await;

// This bit is the same boilerplate as the bit in the first example.
// Nothing to see here.
match result {
Ok(data) => {
let res = create_response(&state, StatusCode::OK, mime::TEXT_PLAIN, data);
println!("sleep for one second {} times: finished", seconds);
Ok((state, res))
}
Err(err) => Err((state, err.into_handler_error())),
}
Err(err) => Err((state, err.into_handler_error())),
}))
};
Box::new(async_block_future.boxed().compat())
}

/// Create a `Router`.
Expand Down

0 comments on commit 309c89f

Please sign in to comment.