-
Notifications
You must be signed in to change notification settings - Fork 321
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
Conversation
93f6ccd
to
3ab3a72
Compare
Overall this seems like a good direction to me. |
f39de2a
to
089e996
Compare
examples/multi_listener.rs
Outdated
)) | ||
}); | ||
|
||
app.listen(vec!["localhost:8000", "localhost:8081"]).await?; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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`]. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice catch!
src/listener/multi_listener.rs
Outdated
///``` | ||
|
||
#[derive(Default)] | ||
pub struct MultiListener<State>(Vec<Box<dyn Listener<State>>>); |
There was a problem hiding this comment.
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".
There was a problem hiding this comment.
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
#[derive(Debug)] | ||
pub enum TcpListener { | ||
FromListener(net::TcpListener), | ||
FromAddrs(Vec<SocketAddr>, Option<net::TcpListener>), | ||
} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
impl<State: Send + Sync + 'static> ToListener<State> for ParsedListener { | ||
type Listener = Self; | ||
fn to_listener(self) -> io::Result<Self::Listener> { | ||
Ok(self) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What does parsedlistener do?
There was a problem hiding this comment.
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
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>; | ||
} |
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM!
and emit multiple logs
The idea is that it should be possible for a third-party crate to implement
tide::listener::ToListener
andtide::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