diff --git a/Cargo.toml b/Cargo.toml index 7c2fed96d..dc6122c2c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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", diff --git a/examples/handlers/README.md b/examples/handlers/README.md index 486ed38ed..fa98ba9a8 100644 --- a/examples/handlers/README.md +++ b/examples/handlers/README.md @@ -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 diff --git a/examples/handlers/simple_async_handlers/Cargo.toml b/examples/handlers/simple_async_handlers/Cargo.toml index 33a242534..0cb8e6a0e 100644 --- a/examples/handlers/simple_async_handlers/Cargo.toml +++ b/examples/handlers/simple_async_handlers/Cargo.toml @@ -4,6 +4,7 @@ description = "An example that does asynchronous work before responding" version = "0.0.0" authors = ["David Laban "] publish = false +edition = "2018" [dependencies] gotham = { path = "../../../gotham" } diff --git a/examples/handlers/simple_async_handlers/README.md b/examples/handlers/simple_async_handlers/README.md index c06af72b4..b0993ca1c 100644 --- a/examples/handlers/simple_async_handlers/README.md +++ b/examples/handlers/simple_async_handlers/README.md @@ -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: diff --git a/examples/handlers/simple_async_handlers_await/Cargo.toml b/examples/handlers/simple_async_handlers_await/Cargo.toml index 33a242534..adb7666ee 100644 --- a/examples/handlers/simple_async_handlers_await/Cargo.toml +++ b/examples/handlers/simple_async_handlers_await/Cargo.toml @@ -1,9 +1,10 @@ [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 "] publish = false +edition = "2018" [dependencies] gotham = { path = "../../../gotham" } @@ -11,7 +12,12 @@ 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" diff --git a/examples/handlers/simple_async_handlers_await/README.md b/examples/handlers/simple_async_handlers_await/README.md index c06af72b4..ee8f9eb10 100644 --- a/examples/handlers/simple_async_handlers_await/README.md +++ b/examples/handlers/simple_async_handlers_await/README.md @@ -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 diff --git a/examples/handlers/simple_async_handlers_await/src/main.rs b/examples/handlers/simple_async_handlers_await/src/main.rs index 4317b2dfc..65ea91cb9 100644 --- a/examples/handlers/simple_async_handlers_await/src/main.rs +++ b/examples/handlers/simple_async_handlers_await/src/main.rs @@ -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; @@ -25,7 +28,7 @@ use gotham::state::{FromState, State}; use tokio::timer::Delay; -type SleepFuture = Box, Error = HandlerError> + Send>; +type SleepFuture = Box, Error = HandlerError> + Send>; #[derive(Deserialize, StateData, StaticResponseExtender)] struct QueryStringExtractor { @@ -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) @@ -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 { - 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 { - 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`.