Skip to content

rfc: Promote low-level server API as the main API #2321

Closed
@LucioFranco

Description

@LucioFranco

Context

Currently, the hyper server module has two major components. A high-level server API and a low level connection based API. The most common usages of hyper for its server come from the high-level abstraction, as it is mostly plug and play. That said, there is a major flaw with the high-level API.

Problem

The high-level API takes some MakeService<T, R> and will call MakeService::make_service with a &IO where IO is some AsyncRead + AsyncWrite. This then allows the user to basically await on a single future for the entire server. Accepting new connections and passing them to the MakeService is all done on the root server future. This means that the Server can continue to use a &mut MakeService reference, calling poll_ready to apply back-pressure without the need for synchronization. Once a connection has been accepted and MakeService returns the per-connection M::Service, hyper will then spawn a connection task that will then handle the connection from that point on. This processing happens off of the root accept task.

This works great when you just want a one liner server and it can support the occasional user that may want to inspect the IO type before processing the connection. For example, it is possible to pull the remote address from the &IO type and give each connection this context in the returned service. When just using a bare TcpStream/TcpListener this works great. This is due to the fact that all processing of the connection happens during the accept phase and not after. When introducing TLS we must continue to an additional per connection processing (handshake). This processing can take some time and thus if done during the accept phase, it may potentially lead to other connections stalling during their accept/connect phase. To solve this, one would need to process the TLS handshake on the connection task, see #2175 for more information on this specifically.

The problem comes up when you want to use the MakeService with each IO type without any synchronization. Ideally, we'd like to accept the tcp connection, then process the handshake in an async manner, then somehow call MakeService::make_service with the TlsStream<TcpStream>. This would allow users to extract the remote address and peer certs. By unfortunately this style is not really compatible with how MakeService::poll_ready is stateless.

Solutions

The solution I would like to propose is to continue to treat hyper as a low level http implementation. This would mean remove the need for the MakeService abstraction but instead promote the low level conn module as the main way to work with servers in hyper. This provides a few advantages, the API is simpler and easier to discover, people can work with async/await instead of poll fn and 'static futures.

Examples

Simple one liners:

serve_fn("0.0.0.0:0", |req| async move { response(req).await }).await;

// Or with a `tower::Service`
serve("0.0.0.0:0", svc).await;

More complex with fetching the local addr from the bind:

let http = Http::new()
	.bind(addr)
	.await
	.unwrap();

let local_addr = http.local_addr();

http.serve_fn(|req| //...).await;

// Or with a `tower::Service`
http.serve(svc).await;

This style, then would allow us in hyper-tls to expand and provide nice and easy abstractions to implement similar on-top of the selected TLS implementation.

A more complex example would be a manual implementation with the Http::service_connection API.

let listener = TcpListener::bind(addr).await?;

let http = Http::new();

let svc = MyHandlerService::new();

loop {
	let (conn, addr) = listener.accept().await?;

	let http = http.clone();
	tokio::spawn(async move {
		// Now we can accept TLS/do other fun things here!

		http.serve_connection(conn, svc.clone()).await.unwrap();
	});
}

This change would allow us to remove the need for MakeService and its confusing abstraction while still leveraging the more powerful aspects of tower.

Rename Http to Server

Now that we are nixing the Server type I'd like for us to rename Http into Server. This would allow us to treat Server as a configuration that is easily clonable. It can then produce a Serve type via Server::bind. The Serve type would handle the default implementation of the server and expose methods to serve a serve or a fn via serve_fn.

Other solutions

We could continue to keep our current implementation and use something like a FuturesUnordered + tokio::spawn but this to me feels like continuing down a hacky path instead of backing out and looking at the bigger picture.

cc @seanmonstar @hawkw @davidbarsky

Metadata

Metadata

Assignees

Labels

A-serverArea: server.C-featureCategory: feature. This is adding a new feature.

Type

No type

Projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions