extensible entrypoint for app.listen#610
Conversation
93f6ccd to
3ab3a72
Compare
|
Overall this seems like a good direction to me. |
f39de2a to
089e996
Compare
| )) | ||
| }); | ||
|
|
||
| app.listen(vec!["localhost:8000", "localhost:8081"]).await?; |
There was a problem hiding this comment.
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.
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.
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
| //! | ||
| //! If the endpoint needs to share values with middleware, response scoped state can be set via | ||
| //! [`Response::set_ext`] and is available through [`Response::ext`]. | ||
| //! [`Response::insert_ext`] and is available through [`Response::ext`]. |
src/listener/multi_listener.rs
Outdated
| ///``` | ||
|
|
||
| #[derive(Default)] | ||
| pub struct MultiListener<State>(Vec<Box<dyn Listener<State>>>); |
There was a problem hiding this comment.
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.
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.
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.
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.
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.
What does parsedlistener do?
There was a problem hiding this comment.
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.
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.
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
and emit multiple logs
The idea is that it should be possible for a third-party crate to implement
tide::listener::ToListenerandtide::listener::Listenerfor 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