Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(volo-http): support extract in timeout handler #268

Merged
merged 2 commits into from
Dec 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 0 additions & 3 deletions examples/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,6 @@ path = "src/http/simple.rs"
anyhow.workspace = true
async-stream.workspace = true
bytes.workspace = true
http = "1"
http-body-util = "0.1"
hyper = { version = "1", features = ["server", "http1", "http2"] }
lazy_static.workspace = true
metainfo.workspace = true
motore.workspace = true
Expand Down
21 changes: 5 additions & 16 deletions examples/src/http/simple.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,10 @@
use std::{net::SocketAddr, time::Duration};

use bytes::Bytes;
use http::{Method, Response, StatusCode, Uri};
use http_body_util::Full;
use serde::Deserialize;
use volo_http::{
layer::TimeoutLayer,
param::Params,
request::Json,
route::{get, post, MethodRouter, Router},
server::Server,
HttpContext,
Address, Bytes, Json, Method, Params, Server, StatusCode, Uri,
};

async fn hello() -> &'static str {
Expand All @@ -32,9 +26,9 @@
}

async fn json(Json(request): Json<Person>) {
let first_phone = request
.phones
.get(0)

Check warning on line 31 in examples/src/http/simple.rs

View workflow job for this annotation

GitHub Actions / clippy

accessing first element with `request .phones.get(0)`

warning: accessing first element with `request .phones.get(0)` --> examples/src/http/simple.rs:29:23 | 29 | let first_phone = request | _______________________^ 30 | | .phones 31 | | .get(0) | |_______________^ | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#get_first = note: `#[warn(clippy::get_first)]` on by default help: try | 29 ~ let first_phone = request 30 + .phones.first() |
.map(|p| p.as_str())
.unwrap_or("no number");
println!(
Expand All @@ -46,7 +40,7 @@
async fn test(u: Uri, m: Method) -> Result<&'static str, (StatusCode, &'static str)> {
println!("uri: {u:?}");
println!("method: {m:?}");
if u.to_string().ends_with("a") {

Check warning on line 43 in examples/src/http/simple.rs

View workflow job for this annotation

GitHub Actions / clippy

single-character string constant used as pattern

warning: single-character string constant used as pattern --> examples/src/http/simple.rs:43:32 | 43 | if u.to_string().ends_with("a") { | ^^^ help: try using a `char` instead: `'a'` | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#single_char_pattern = note: `#[warn(clippy::single_char_pattern)]` on by default
Ok("a") // http://localhost:3000/test?a=a
} else {
Err((StatusCode::BAD_REQUEST, "b")) // http://localhost:3000/test?a=bb
Expand All @@ -57,8 +51,8 @@
tokio::time::sleep(Duration::from_secs(5)).await
}

fn timeout_handler(ctx: &HttpContext) -> StatusCode {
tracing::info!("Timeout on `{}`, peer: {}", ctx.uri, ctx.peer);
fn timeout_handler(uri: Uri, peer: Address) -> StatusCode {
tracing::info!("Timeout on `{}`, peer: {}", uri, peer);
StatusCode::INTERNAL_SERVER_ERROR
}

Expand All @@ -70,16 +64,11 @@

tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed");

let timeout_response = Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(Full::new(Bytes::new()))
.unwrap();

let app = Router::new()
.route(
"/",
get(hello).layer(TimeoutLayer::new(Duration::from_secs(1), move |_| {
timeout_response
get(hello).layer(TimeoutLayer::new(Duration::from_secs(1), || {
StatusCode::INTERNAL_SERVER_ERROR
})),
)
.route("/:echo", get(echo))
Expand Down
12 changes: 12 additions & 0 deletions volo-http/src/extract.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use futures_util::Future;
use http::{Method, Response, Uri};
use volo::net::Address;

use crate::{response::IntoResponse, HttpContext, Params, State};

Expand All @@ -23,6 +24,17 @@ where
}
}

impl<S> FromContext<S> for Address
where
S: Send + Sync,
{
type Rejection = Response<()>; // Infallible

async fn from_context(context: &HttpContext, _state: &S) -> Result<Address, Self::Rejection> {
Ok(context.peer.clone())
}
}

impl<S> FromContext<S> for Uri
where
S: Send + Sync,
Expand Down
67 changes: 42 additions & 25 deletions volo-http/src/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use motore::Service;

use crate::{
extract::FromContext,
macros::{all_the_tuples, all_the_tuples_no_last_special_case},
request::FromRequest,
response::{IntoResponse, RespBody},
DynError, DynService, HttpContext,
Expand Down Expand Up @@ -79,31 +80,7 @@ macro_rules! impl_handler {
};
}

impl_handler!([], T1);
impl_handler!([T1], T2);
impl_handler!([T1, T2], T3);
impl_handler!([T1, T2, T3], T4);
impl_handler!([T1, T2, T3, T4], T5);
impl_handler!([T1, T2, T3, T4, T5], T6);
impl_handler!([T1, T2, T3, T4, T5, T6], T7);
impl_handler!([T1, T2, T3, T4, T5, T6, T7], T8);
impl_handler!([T1, T2, T3, T4, T5, T6, T7, T8], T9);
impl_handler!([T1, T2, T3, T4, T5, T6, T7, T8, T9], T10);
impl_handler!([T1, T2, T3, T4, T5, T6, T7, T8, T9, T10], T11);
impl_handler!([T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11], T12);
impl_handler!([T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12], T13);
impl_handler!(
[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13],
T14
);
impl_handler!(
[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14],
T15
);
impl_handler!(
[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15],
T16
);
all_the_tuples!(impl_handler);

// Use an extra trait with less generic types for hiding the type of handler
pub struct DynHandler<S>(Box<dyn ErasedIntoRoute<S>>);
Expand Down Expand Up @@ -272,3 +249,43 @@ where
Ok(self.handler.clone().call(cx, req, &self.state).await)
}
}

pub trait HandlerWithoutRequest<T>: Sized {
fn call(self, context: &HttpContext) -> impl Future<Output = Response<RespBody>> + Send;
}

impl<F, Res> HandlerWithoutRequest<()> for F
where
F: FnOnce() -> Res + Clone + Send,
Res: IntoResponse,
{
async fn call(self, _context: &HttpContext) -> Response<RespBody> {
self().into_response()
}
}

macro_rules! impl_handler_without_request {
(
$($ty:ident),* $(,)?
) => {
#[allow(non_snake_case, unused_mut, unused_variables)]
impl<F, Res, $($ty,)*> HandlerWithoutRequest<($($ty,)*)> for F
where
F: FnOnce($($ty,)*) -> Res + Clone + Send,
Res: IntoResponse,
$( for<'r> $ty: FromContext<()> + Send + 'r, )*
{
async fn call(self, context: &HttpContext) -> Response<RespBody> {
$(
let $ty = match $ty::from_context(context, &()).await {
Ok(value) => value,
Err(rejection) => return rejection.into_response(),
};
)*
self($($ty,)*).into_response()
}
}
};
}

all_the_tuples_no_last_special_case!(impl_handler_without_request);
44 changes: 26 additions & 18 deletions volo-http/src/layer.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
use std::time::Duration;
use std::{marker::PhantomData, time::Duration};

use http::{Method, Request, Response, StatusCode};
use http_body_util::Full;
use hyper::body::{Bytes, Incoming};
use motore::{layer::Layer, service::Service};

use crate::{
handler::HandlerWithoutRequest,
response::{IntoResponse, RespBody},
HttpContext,
};
Expand All @@ -14,7 +15,7 @@
fn method(
self,
method: Method,
) -> FilterLayer<Box<dyn Fn(&mut HttpContext, &Request<Incoming>) -> Result<(), StatusCode>>>

Check warning on line 18 in volo-http/src/layer.rs

View workflow job for this annotation

GitHub Actions / clippy

very complex type used. Consider factoring parts into `type` definitions

warning: very complex type used. Consider factoring parts into `type` definitions --> volo-http/src/layer.rs:18:10 | 18 | ) -> FilterLayer<Box<dyn Fn(&mut HttpContext, &Request<Incoming>) -> Result<(), StatusCode>>> | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#type_complexity = note: `#[warn(clippy::type_complexity)]` on by default
where
Self: Sized,
{
Expand Down Expand Up @@ -93,50 +94,57 @@
}

#[derive(Clone)]
pub struct TimeoutLayer<F> {
pub struct TimeoutLayer<H, T> {
duration: Duration,
handler: F,
handler: H,
_marker: PhantomData<T>,
}

impl<F> TimeoutLayer<F> {
pub fn new<T>(duration: Duration, handler: F) -> Self
impl<H, T> TimeoutLayer<H, T> {
pub fn new(duration: Duration, handler: H) -> Self
where
F: FnOnce(&HttpContext) -> T + Clone + Sync,
T: IntoResponse,
H: HandlerWithoutRequest<T> + Clone + Send + Sync + 'static,
{
Self { duration, handler }
Self {
duration,
handler,
_marker: PhantomData,
}
}
}

impl<S, F, T> Layer<S> for TimeoutLayer<F>
impl<S, H, T> Layer<S> for TimeoutLayer<H, T>
where
S: Service<HttpContext, Incoming, Response = Response<RespBody>> + Send + Sync + 'static,
F: FnOnce(&HttpContext) -> T + Clone + Sync,
T: IntoResponse,
H: HandlerWithoutRequest<T> + Clone + Send + Sync + 'static,
T: Sync,
{
type Service = Timeout<S, F>;
type Service = Timeout<S, H, T>;

fn layer(self, inner: S) -> Self::Service {
Timeout {
service: inner,
duration: self.duration,
handler: self.handler,
_marker: PhantomData,
}
}
}

#[derive(Clone)]
pub struct Timeout<S, F> {
pub struct Timeout<S, H, T> {
service: S,
duration: Duration,
handler: F,
handler: H,
_marker: PhantomData<T>,
}

impl<S, F, T> Service<HttpContext, Incoming> for Timeout<S, F>
impl<S, H, T> Service<HttpContext, Incoming> for Timeout<S, H, T>
where
S: Service<HttpContext, Incoming, Response = Response<RespBody>> + Send + Sync + 'static,
F: FnOnce(&HttpContext) -> T + Clone + Sync,
T: IntoResponse,
S::Error: Send,
H: HandlerWithoutRequest<T> + Clone + Send + Sync + 'static,
T: Sync,
{
type Response = S::Response;

Expand All @@ -153,7 +161,7 @@
tokio::select! {
resp = fut_service => resp,
_ = fut_timeout => {
Ok((self.handler.clone())(cx).into_response())
Ok(self.handler.clone().call(cx).await.into_response())
},
}
}
Expand Down
11 changes: 8 additions & 3 deletions volo-http/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,15 @@ pub mod response;
pub mod route;
pub mod server;

use http::{Extensions, HeaderMap, HeaderValue, Method, Uri, Version};
mod macros;

pub use bytes::Bytes;
use http::{Extensions, HeaderMap, HeaderValue, Version};
pub use http::{Method, StatusCode, Uri};
use hyper::{body::Incoming, Response};
use param::Params;
use volo::net::Address;
pub use volo::net::Address;

pub use crate::{param::Params, request::Json, server::Server};

mod private {
#[derive(Debug, Clone, Copy)]
Expand Down
46 changes: 46 additions & 0 deletions volo-http/src/macros.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#[rustfmt::skip]
macro_rules! all_the_tuples {
($name:ident) => {
$name!([], T1);
$name!([T1], T2);
$name!([T1, T2], T3);
$name!([T1, T2, T3], T4);
$name!([T1, T2, T3, T4], T5);
$name!([T1, T2, T3, T4, T5], T6);
$name!([T1, T2, T3, T4, T5, T6], T7);
$name!([T1, T2, T3, T4, T5, T6, T7], T8);
$name!([T1, T2, T3, T4, T5, T6, T7, T8], T9);
$name!([T1, T2, T3, T4, T5, T6, T7, T8, T9], T10);
$name!([T1, T2, T3, T4, T5, T6, T7, T8, T9, T10], T11);
$name!([T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11], T12);
$name!([T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12], T13);
$name!([T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13], T14);
$name!([T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14], T15);
$name!([T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15], T16);
};
}

#[rustfmt::skip]
macro_rules! all_the_tuples_no_last_special_case {
($name:ident) => {
$name!(T1);
$name!(T1, T2);
$name!(T1, T2, T3);
$name!(T1, T2, T3, T4);
$name!(T1, T2, T3, T4, T5);
$name!(T1, T2, T3, T4, T5, T6);
$name!(T1, T2, T3, T4, T5, T6, T7);
$name!(T1, T2, T3, T4, T5, T6, T7, T8);
$name!(T1, T2, T3, T4, T5, T6, T7, T8, T9);
$name!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10);
$name!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11);
$name!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12);
$name!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13);
$name!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14);
$name!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15);
$name!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16);
};
}

pub(crate) use all_the_tuples;
pub(crate) use all_the_tuples_no_last_special_case;
Loading