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

extensible entrypoint for app.listen #610

Merged
merged 14 commits into from
Jul 13, 2020
Merged

extensible entrypoint for app.listen #610

merged 14 commits into from
Jul 13, 2020

Conversation

jbr
Copy link
Member

@jbr jbr commented Jun 21, 2020

image
image
image

The idea is that it should be possible for a third-party crate to implement tide::listener::ToListener and tide::listener::Listener for any newtype (eg multiple varieties of TlsListener) and support passing it to Server::listen and adding it to a MultiListener alongside other Listeners.

Working experimental example of tls on tide: https://github.com/jbr/tide-tls


Note for future changes:

In order to provide hard/soft concurrency limits on open requests as per #586, the Listener trait will likely need to change to include a hook of some sort for backpressure, but that was beyond the scope of this already-large PR. Similarly, the Listener trait would be a good place to introduce a graceful shutdown mechanism

@jbr jbr added the enhancement New feature or request label Jun 21, 2020
@jbr jbr force-pushed the to-listener branch 3 times, most recently from 93f6ccd to 3ab3a72 Compare June 22, 2020 17:14
@jbr jbr changed the title WIP: single extensible entrypoint for app.listen single extensible entrypoint for app.listen Jun 24, 2020
@jbr jbr changed the title single extensible entrypoint for app.listen extensible entrypoint for app.listen Jun 24, 2020
@jbr jbr marked this pull request as ready for review June 24, 2020 02:52
@jbr jbr requested a review from yoshuawuyts June 24, 2020 02:53
src/listener/tcp_listener.rs Show resolved Hide resolved
src/listener/to_listener.rs Show resolved Hide resolved
src/listener/to_listener.rs Show resolved Hide resolved
@Fishrock123
Copy link
Member

Overall this seems like a good direction to me.

@jbr jbr force-pushed the to-listener branch 2 times, most recently from f39de2a to 089e996 Compare June 25, 2020 21:42
))
});

app.listen(vec!["localhost:8000", "localhost:8081"]).await?;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would interpret this as: "try listening on port 8000 first, if that's not possible run on port 8081". This is how ToSocketAddr for [SocketAddr] works in std as well.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally, that seems like a surprising behavior, try_listen seems like a better name for that but I suppose that could already be idiomatic in std rust.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I started writing an implementation of the fallback port multi-listener, but I realized I don't understand the use case. When ToSocketAddr resolves to multiple ports, those ports represent different notions of the same thing, so like binding to [::1]:8000 vs 127.0.0.1:8000 are both representing the notion of "localhost:8000" so a system could reliably provide "localhost:8000" and then access it from port 8000 by reverse proxying to that port, for example. However, providing a vec of ["localhost:8000", "localhost:8001", "localhost:8002"] does not represent one resource, and I don't understand how the behavior of fallback ports would actually be used. If 8000 is currently bound, isn't that a concerning state that should halt app startup?

In comparison, binding to multiple ports concurrently has several slightly-more-common use cases, such as supporting both uds for fast clients (local to the machine) also to tcp for network clients, or binding to both a public network interface as well as a private network interface, or potentially binding to both tcp://0.0.0.0:80 and tls://0.0.0.0:443 with appropriate permissions (authbind, etc).

src/lib.rs Outdated
//! [`Response::set_ext`] and is available through [`Response::ext`].
//! [`Response::insert_ext`] and is available through [`Response::ext`].
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch!

src/listener/mod.rs Show resolved Hide resolved
src/listener/parsed_listener.rs Show resolved Hide resolved
///```

#[derive(Default)]
pub struct MultiListener<State>(Vec<Box<dyn Listener<State>>>);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something we talked about yesterday was that it'd be nice to find a way to add functionality as "listen on multiple listeners" and "attempt to connect to these listeners in this sequence".

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addressed by renaming MultiListener to ConcurrentListener and adding FailoverListener. I don't understand the use case well enough to write good/representative docs or examples, though: https://github.com/http-rs/tide/pull/610/files#diff-92873655898e8cfdba457e044d652af1 Importantly, this sort of thing would be easy for apps to implement on their own, or third party crates, etc. I think the only tide-internal question is what Vec turns into

src/listener/to_listener.rs Outdated Show resolved Hide resolved
Comment on lines +13 to +24
#[derive(Debug)]
pub enum TcpListener {
FromListener(net::TcpListener),
FromAddrs(Vec<SocketAddr>, Option<net::TcpListener>),
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I guess I'm somewhat confused by this structure. In the stdlib ToSocketAddr is resolved the moment it is created. Is there a practical reason we're not doing the same here?

That way we can store a single async_std::net::TcpListener that we then later listen on. The same goes for unix_listener.rs. ToListener::to_listener is fallible, so I don't believe it should be a problem.

Copy link
Member Author

@jbr jbr Jun 26, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The difference is that ToSocketAddr doesn't immediately bind to that address. In this version of the code, we do as much resolution as we can, but we avoid binding until listen is called. That way this doesn't exhibit confusing behavior:

let mut multi = MultiListener::new();
multi.add("localhost:8000")?;
multi.add("unix+http://file")?;
// are 8000 and ./file bound here? I wouldn't expect them to be yet

multi.listen().await?; //but now I would expect them to be.

If this is not the case, MultiListener::add is actually MultiListener::listen which would be a different approach. In this design, there's a state distinction between a Listener that can listen and a bound transport

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also would not expect resources by string to be opened until listen is called, unless someone passes in something that is already explicitly bound, i.e. a struct for a file descriptor or something.

src/listener/to_listener.rs Outdated Show resolved Hide resolved
impl<State: Send + Sync + 'static> ToListener<State> for ParsedListener {
type Listener = Self;
fn to_listener(self) -> io::Result<Self::Listener> {
Ok(self)
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does parsedlistener do?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was a simple approach to wrapping two specific listener types without having to make them dyn. We can only parse a limited number of types currently, so this is the associated Listener type that's returned from string parsing

Comment on lines +56 to +64
pub trait ToListener<State: Send + Sync + 'static> {
type Listener: Listener<State>;
/// Transform self into a
/// [`Listener`](crate::listener::Listener). Unless self is
/// already bound/connected to the underlying io, converting to a
/// listener does not initiate a connection. An Err return
/// indicates an unsuccessful conversion to a listener, not an
/// unsuccessful bind attempt.
fn to_listener(self) -> io::Result<Self::Listener>;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One question that I have is: why don't we perform the listening logic inside of the to_listener function's body? Intuitively it feels like we may be having one layer of traits more than is required. We can still reuse logic by having an e.g. listen_unix function that knows how to listen on a unix socket, but that can be called from this function.

I guess what I'm trying to say is that I'm unclear why "convert into a listener" and "listen on said listener" are two separate steps. My guess is that it's probably coming from the desire to have MultiListener?

During our chat yesterday I showed how to manually MultiListener's functionality using task::spawn and future::join. We agreed that doing this manually isn't ideal, so having an intermediate abstraction makes sense. But what I think that shows is that servers that have been started can in fact be combined after the fact.

I'm not entirely convinced that I've quite covered all the details here -- but I feel like there might be something to it. Would be keen to hear your thoughts on this!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made an experimental branch at https://github.com/jbr/tide/tree/simpler-listener-trait for comparison. Docs haven't been updated, but: There's no longer a ToListener trait, just a Listener trait. Calling Listener::<State>::listen("http://localhost:8000").await immediately binds, and Listener is implemented for all of the types that ToListener had been implemented for. MultiListener also binds immediately, though, which feels like a step backwards. I couldn't figure out how to get it to correctly hold the Box<dyn Listener>s. Listen needs to take self because some listeners need to move self into the listener.

I still prefer the two-step version, but it's true that there's slightly less code in the one-trait version: jbr@e86e957

Cargo.toml Outdated Show resolved Hide resolved
Copy link
Member

@yoshuawuyts yoshuawuyts left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@jbr jbr merged commit ba65ca4 into http-rs:master Jul 13, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants