Skip to content

Commit

Permalink
state: move instantiation into State::from_request (#526)
Browse files Browse the repository at this point in the history
This refactors the service implementation a little bit to instantiate
the State via a dedicated method. The underlying goal is to allow users
to use Gotham without having to use its handler.

The motivation for that is to allow:

- Not requiring handlers to be unwind safe.
- Injecting extra things into the state that came from the socket
  (besides the client address), such as client TLS certificates.
  • Loading branch information
krallin authored Mar 1, 2021
1 parent 54b81e7 commit 7f62356
Show file tree
Hide file tree
Showing 8 changed files with 169 additions and 51 deletions.
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -81,5 +81,7 @@ members = [
# finalizer
"examples/finalizers/",

# custom_service
"examples/custom_service/",

]
14 changes: 14 additions & 0 deletions examples/custom_service/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[package]
name = "gotham_examples_custom_service"
description = "Using Gotham in a pre-existing Hyper service"
version = "0.0.0"
publish = false
edition = "2018"

[dependencies]
anyhow = "1.0"
futures = "0.3"
gotham = { path = "../../gotham" }
http = "0.2"
hyper = { version = "0.14", features = ["full"] }
tokio = { version = "1.0", features = ["full"] }
19 changes: 19 additions & 0 deletions examples/custom_service/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Custom Service

An example of using Gotham within a pre-existing Hyper service. This is useful
for advanced use cases where you might want to wrap Gotham in order logic, or
pre-populate your state handing it off to Gotham.

## 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)
78 changes: 78 additions & 0 deletions examples/custom_service/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
//! An example usage of Gotham from another service.
use anyhow::{Context as _, Error};
use futures::future::{BoxFuture, FutureExt};
use gotham::{
router::{builder::*, Router},
service::call_handler,
state::State,
};
use http::{Request, Response};
use hyper::{server::conn::Http, service::Service, Body};
use std::net::SocketAddr;
use std::panic::AssertUnwindSafe;
use std::task;
use tokio::net::TcpListener;

#[derive(Clone)]
struct MyService {
router: Router,
addr: SocketAddr,
}

impl Service<Request<Body>> for MyService {
type Response = Response<Body>;
type Error = Error;
type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;

fn poll_ready(&mut self, _cx: &mut task::Context<'_>) -> task::Poll<Result<(), Self::Error>> {
task::Poll::Ready(Ok(()))
}

fn call(&mut self, req: Request<Body>) -> Self::Future {
// NOTE: You don't *have* to use call_handler for this (you could use `router.handle`), but
// call_handler will catch panics and return en error response.
let state = State::from_request(req, self.addr);
call_handler(self.router.clone(), AssertUnwindSafe(state)).boxed()
}
}

pub fn say_hello(state: State) -> (State, &'static str) {
(state, "hello world")
}

#[tokio::main]
pub async fn main() -> Result<(), Error> {
let router = build_simple_router(|route| {
// For the path "/" invoke the handler "say_hello"
route.get("/").to(say_hello);
});

let addr = "127.0.0.1:7878";
let listener = TcpListener::bind(&addr).await?;

println!("Listening for requests at http://{}", addr);

loop {
let (socket, addr) = listener
.accept()
.await
.context("Error accepting connection")?;

let service = MyService {
router: router.clone(),
addr,
};

let task = async move {
Http::new()
.serve_connection(socket, service)
.await
.context("Error serving connection")?;

Result::<_, Error>::Ok(())
};

tokio::spawn(task);
}
}
2 changes: 1 addition & 1 deletion gotham/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ pub mod helpers;
pub mod middleware;
pub mod pipeline;
pub mod router;
mod service;
pub mod service;
pub mod state;

/// Test utilities for Gotham and Gotham consumer apps.
Expand Down
50 changes: 5 additions & 45 deletions gotham/src/service/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,19 @@ use std::net::SocketAddr;
use std::panic::AssertUnwindSafe;
use std::pin::Pin;
use std::sync::Arc;
use std::thread;

use futures::prelude::*;
use futures::task::{self, Poll};
use http::request;
use hyper::service::Service;
use hyper::upgrade::OnUpgrade;
use hyper::{Body, Request, Response};
use log::debug;

use crate::handler::NewHandler;

use crate::helpers::http::request::path::RequestPathSegments;
use crate::state::client_addr::put_client_addr;
use crate::state::{set_request_id, State};
use crate::state::State;

mod trap;

pub use trap::call_handler;

/// Wraps a `NewHandler` which will be used to serve requests. Used in `gotham::os::*` to bind
/// incoming connections to `ConnectedGothamService` values.
pub(crate) struct GothamService<T>
Expand Down Expand Up @@ -76,43 +71,8 @@ where
}

fn call<'a>(&'a mut self, req: Request<Body>) -> Self::Future {
let mut state = State::new();

put_client_addr(&mut state, self.client_addr);

let (
request::Parts {
method,
uri,
version,
headers,
mut extensions,
..
},
body,
) = req.into_parts();

state.put(RequestPathSegments::new(uri.path()));
state.put(method);
state.put(uri);
state.put(version);
state.put(headers);
state.put(body);

if let Some(on_upgrade) = extensions.remove::<OnUpgrade>() {
state.put(on_upgrade);
}

{
let request_id = set_request_id(&mut state);
debug!(
"[DEBUG][{}][Thread][{:?}]",
request_id,
thread::current().id(),
);
};

trap::call_handler(self.handler.clone(), AssertUnwindSafe(state)).boxed()
let state = State::from_request(req, self.client_addr);
call_handler(self.handler.clone(), AssertUnwindSafe(state)).boxed()
}
}

Expand Down
5 changes: 1 addition & 4 deletions gotham/src/service/trap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,7 @@ where
///
/// Timing information is recorded and logged, except in the case of a panic where the timer is
/// moved and cannot be recovered.
pub(super) async fn call_handler<T>(
t: T,
state: AssertUnwindSafe<State>,
) -> anyhow::Result<Response<Body>>
pub async fn call_handler<T>(t: T, state: AssertUnwindSafe<State>) -> anyhow::Result<Response<Body>>
where
T: NewHandler + Send + UnwindSafe,
{
Expand Down
50 changes: 49 additions & 1 deletion gotham/src/state/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,22 @@ mod data;
mod from_state;
pub mod request_id;

use log::trace;
use log::{debug, trace};

use http::request;
use hyper::upgrade::OnUpgrade;
use hyper::{Body, Request};
use std::any::{Any, TypeId};
use std::collections::HashMap;
use std::net::SocketAddr;

pub use crate::state::client_addr::client_addr;
pub use crate::state::data::StateData;
pub use crate::state::from_state::FromState;
pub use crate::state::request_id::request_id;

use crate::helpers::http::request::path::RequestPathSegments;
use crate::state::client_addr::put_client_addr;
pub(crate) use crate::state::request_id::set_request_id;

/// Provides storage for request state, and stores one item of each type. The types used for
Expand Down Expand Up @@ -69,6 +75,48 @@ impl State {
f(&mut State::new())
}

/// Instantiate a new `State` for a given `Request`. This is primarily useful if you're calling
/// Gotham from your own Hyper service.
pub fn from_request(req: Request<Body>, client_addr: SocketAddr) -> Self {
let mut state = Self::new();

put_client_addr(&mut state, client_addr);

let (
request::Parts {
method,
uri,
version,
headers,
mut extensions,
..
},
body,
) = req.into_parts();

state.put(RequestPathSegments::new(uri.path()));
state.put(method);
state.put(uri);
state.put(version);
state.put(headers);
state.put(body);

if let Some(on_upgrade) = extensions.remove::<OnUpgrade>() {
state.put(on_upgrade);
}

{
let request_id = set_request_id(&mut state);
debug!(
"[DEBUG][{}][Thread][{:?}]",
request_id,
std::thread::current().id(),
);
};

state
}

/// Puts a value into the `State` storage. One value of each type is retained. Successive calls
/// to `put` will overwrite the existing value of the same type.
///
Expand Down

0 comments on commit 7f62356

Please sign in to comment.