diff --git a/Cargo.toml b/Cargo.toml index 2315b9fc9..fe3efb2f6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,6 +58,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/README.md b/examples/handlers/simple_async_handlers/README.md index c06af72b4..e7acb56fa 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_await). + ## 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 new file mode 100644 index 000000000..cf7b1ca30 --- /dev/null +++ b/examples/handlers/simple_async_handlers_await/Cargo.toml @@ -0,0 +1,17 @@ +[package] +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" } +gotham_derive = { path = "../../../gotham_derive" } + +mime = "0.3" +futures = "0.3.1" +serde = "1.0" +serde_derive = "1.0" +tokio = "0.2.9" diff --git a/examples/handlers/simple_async_handlers_await/README.md b/examples/handlers/simple_async_handlers_await/README.md new file mode 100644 index 000000000..dd13ad5d8 --- /dev/null +++ b/examples/handlers/simple_async_handlers_await/README.md @@ -0,0 +1,49 @@ +# 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. 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. + +## Running + +From the `examples/handlers/async_handlers` directory: + +``` +Terminal 1: + Compiling gotham_examples_handlers_simple_async_handlers v0.0.0 (file:///.../gotham/examples/handlers/simple_async_handlers) + Finished dev [unoptimized + debuginfo] target(s) in 8.19 secs + Running `.../gotham/target/debug/gotham_examples_handlers_simple_async_handlers` +Listening for requests at http://127.0.0.1:7878 +sleep for 5 seconds once: starting +sleep for 5 seconds once: finished +sleep for one second 5 times: starting +sleep for one second 5 times: finished + +Terminal 2: +$ curl 'http://127.0.0.1:7878/sleep?seconds=5' +slept for 5 seconds +$ curl 'http://127.0.0.1:7878/loop?seconds=5' +slept for 1 seconds +slept for 1 seconds +slept for 1 seconds +slept for 1 seconds +slept for 1 seconds +``` + +## License + +Licensed under your option of: + +* [MIT License](../../LICENSE-MIT) +* [Apache License, Version 2.0](../../LICENSE-APACHE) + +## Community + +The following policies guide participation in our project and our community: + +* [Code of conduct](../../CODE_OF_CONDUCT.md) +* [Contributing](../../CONTRIBUTING.md) diff --git a/examples/handlers/simple_async_handlers_await/src/main.rs b/examples/handlers/simple_async_handlers_await/src/main.rs new file mode 100644 index 000000000..3d0a1449f --- /dev/null +++ b/examples/handlers/simple_async_handlers_await/src/main.rs @@ -0,0 +1,162 @@ +//! A basic example showing the request components + +use futures::prelude::*; +use std::pin::Pin; +use std::time::{Duration, Instant}; + +use gotham::hyper::{Body, StatusCode}; + +use gotham::handler::HandlerResult; +use gotham::helpers::http::response::create_response; +use gotham::router::builder::DefineSingleRoute; +use gotham::router::builder::{build_simple_router, DrawRoutes}; +use gotham::router::Router; +use gotham::state::{FromState, State}; +use gotham_derive::{StateData, StaticResponseExtender}; +use serde_derive::Deserialize; + +use tokio::time::delay_until; + +type SleepFuture = Pin> + Send>>; + +#[derive(Deserialize, StateData, StaticResponseExtender)] +struct QueryStringExtractor { + seconds: u64, +} + +/// Sneaky hack to make tests take less time. Nothing to see here ;-). +#[cfg(not(test))] +fn get_duration(seconds: u64) -> Duration { + Duration::from_secs(seconds) +} +#[cfg(test)] +fn get_duration(seconds: u64) -> Duration { + Duration::from_millis(seconds) +} +/// All this function does is return a future that resolves after a number of +/// seconds, with a Vec that tells you how long it slept for. +/// +/// Note that it does not block the thread from handling other requests, because +/// it returns a `Future`, which will be managed by the tokio reactor, and +/// called back once the timeout has expired. +/// +/// Vec is chosen because it is one of the things that you need to resolve +/// a HandlerFuture and respond to a request. +/// +/// Most things that you call to access remote services (e.g databases and +/// 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. +fn sleep(seconds: u64) -> SleepFuture { + let when = Instant::now() + get_duration(seconds); + let delay = delay_until(when.into()).map(move |_| { + format!("slept for {} seconds\n", seconds) + .as_bytes() + .to_vec() + }); + + delay.boxed() +} + +/// This handler sleeps for the requested number of seconds, using the `sleep()` +/// helper method, above. +async fn sleep_handler(mut state: State) -> HandlerResult { + 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); + + // 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 data = 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. + let res = create_response(&state, StatusCode::OK, mime::TEXT_PLAIN, data); + println!("sleep for {} seconds once: finished", seconds); + Ok((state, res)) +} + +/// It calls sleep(1) as many times as needed to make the requested duration. +/// +/// Notice how much easier it is to read than the version in +/// `simple_async_handlers`. +async fn loop_handler(mut state: State) -> HandlerResult { + let seconds = QueryStringExtractor::take_from(&mut state).seconds; + println!("sleep for one second {} times: starting", seconds); + + // 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).await; + accumulator.extend(body) + } + + let res = create_response( + &state, + StatusCode::OK, + mime::TEXT_PLAIN, + Body::from(accumulator), + ); + println!("sleep for one second {} times: finished", seconds); + Ok((state, res)) +} + +/// Create a `Router`. +fn router() -> Router { + build_simple_router(|route| { + route + .get("/sleep") + .with_query_string_extractor::() + .to_async(sleep_handler); + route + .get("/loop") + .with_query_string_extractor::() + .to_async(loop_handler); + }) +} + +/// Start a server and use a `Router` to dispatch requests. +pub fn main() { + let addr = "127.0.0.1:7878"; + println!("Listening for requests at http://{}", addr); + gotham::start(addr, router()) +} + +#[cfg(test)] +mod tests { + use gotham::test::TestServer; + + use super::*; + + fn assert_returns_ok(url_str: &str, expected_response: &str) { + let test_server = TestServer::new(router()).unwrap(); + let response = test_server.client().get(url_str).perform().unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + assert_eq!( + &String::from_utf8(response.read_body().unwrap()).unwrap(), + expected_response + ); + } + + #[test] + fn sleep_says_how_long_it_slept_for() { + assert_returns_ok("http://localhost/sleep?seconds=2", "slept for 2 seconds\n"); + } + + #[test] + fn loop_breaks_the_time_into_one_second_sleeps() { + assert_returns_ok( + "http://localhost/loop?seconds=2", + "slept for 1 seconds\nslept for 1 seconds\n", + ); + } +} diff --git a/gotham/src/handler/mod.rs b/gotham/src/handler/mod.rs index 948ca4c3f..1a4f452a0 100644 --- a/gotham/src/handler/mod.rs +++ b/gotham/src/handler/mod.rs @@ -23,12 +23,14 @@ pub mod assets; pub use self::error::{HandlerError, IntoHandlerError}; +/// A type alias for the results returned by async fns that can be passed to to_async. +pub type HandlerResult = std::result::Result<(State, Response), (State, HandlerError)>; + /// A type alias for the trait objects returned by `HandlerService`. /// /// When the `Future` resolves to an error, the `(State, HandlerError)` value is used to generate /// an appropriate HTTP error response. -pub type HandlerFuture = - dyn Future), (State, HandlerError)>> + Send; +pub type HandlerFuture = dyn Future + Send; /// A `Handler` is an asynchronous function, taking a `State` value which represents the request /// and related runtime state, and returns a future which resolves to a response. diff --git a/gotham/src/router/builder/single.rs b/gotham/src/router/builder/single.rs index 8f9f3ba03..371fb1fb9 100644 --- a/gotham/src/router/builder/single.rs +++ b/gotham/src/router/builder/single.rs @@ -4,7 +4,7 @@ use std::panic::RefUnwindSafe; use crate::extractor::{PathExtractor, QueryStringExtractor}; use crate::handler::assets::{DirHandler, FileHandler, FileOptions, FilePathExtractor}; -use crate::handler::{Handler, NewHandler}; +use crate::handler::{Handler, HandlerResult, NewHandler}; use crate::pipeline::chain::PipelineHandleChain; use crate::router::builder::{ ExtendRouteMatcher, ReplacePathExtractor, ReplaceQueryStringExtractor, SingleRouteBuilder, @@ -12,6 +12,9 @@ use crate::router::builder::{ use crate::router::route::dispatch::DispatcherImpl; use crate::router::route::matcher::RouteMatcher; use crate::router::route::{Delegation, Extractors, RouteImpl}; +use crate::state::State; +use core::future::Future; +use futures::FutureExt; /// Describes the API for defining a single route, after determining which request paths will be /// dispatched here. The API here uses chained function calls to build and add the route into the @@ -106,6 +109,54 @@ pub trait DefineSingleRoute { where H: Handler + RefUnwindSafe + Copy + Send + Sync + 'static; + /// Similar to `to`, but accepts an `async fn` + /// + /// # Examples + /// + /// ```rust + /// # extern crate gotham; + /// # extern crate hyper; + /// # + /// # use hyper::{Body, Response, StatusCode}; + /// # use gotham::handler::HandlerResult; + /// # use gotham::state::State; + /// # use gotham::router::Router; + /// # use gotham::router::builder::*; + /// # use gotham::pipeline::new_pipeline; + /// # use gotham::pipeline::single::*; + /// # use gotham::middleware::session::NewSessionMiddleware; + /// # use gotham::test::TestServer; + /// # + /// async fn my_handler(state: State) -> HandlerResult { + /// // Handler implementation elided. + /// # Ok((state, Response::builder().status(StatusCode::ACCEPTED).body(Body::empty()).unwrap())) + /// } + /// # + /// # fn router() -> Router { + /// # let (chain, pipelines) = single_pipeline( + /// # new_pipeline().add(NewSessionMiddleware::default()).build() + /// # ); + /// + /// build_router(chain, pipelines, |route| { + /// route.get("/request/path").to_async(my_handler); + /// }) + /// # + /// # } + /// # + /// # fn main() { + /// # let test_server = TestServer::new(router()).unwrap(); + /// # let response = test_server.client() + /// # .get("https://example.com/request/path") + /// # .perform() + /// # .unwrap(); + /// # assert_eq!(response.status(), StatusCode::ACCEPTED); + /// # } + /// ``` + fn to_async(self, handler: H) + where + Self: Sized, + H: (FnOnce(State) -> Fut) + RefUnwindSafe + Copy + Send + Sync + 'static, + Fut: Future + Send + 'static; /// Directs the route to the given `NewHandler`. This gives more control over how `Handler` /// values are constructed. /// @@ -471,6 +522,15 @@ where self.to_new_handler(move || Ok(handler)) } + fn to_async(self, handler: H) + where + Self: Sized, + H: (FnOnce(State) -> Fut) + RefUnwindSafe + Copy + Send + Sync + 'static, + Fut: Future + Send + 'static, + { + self.to_new_handler(move || Ok(move |s: State| handler(s).boxed())) + } + fn to_new_handler(self, new_handler: NH) where NH: NewHandler + 'static,