`Graceful` struct is not accessible
Hello, I believe I have found a bug/inconsistency between the Server::with_graceful_shutdown function and the Graceful struct : the former is exposed as part of the public API, and returns a Graceful object, although the latter is not publicly accessible (as it is part of the shutdown private module).
This causes problems when trying to use the graceful shutdown capabilities and passing a Graceful object across functions, as an argument or return value of type Graceful in a custom function cannot be properly typed.
It's meant to be a private type. You can use it as a generic Future.
Thank you for the answer. I am curious as to what the reasoning behind that choice is, what are the reasons to keep the type private if its instances are publicly accessible ? I am under the impression that many Rust users, like me, are disconcerted by such a pattern (https://www.reddit.com/r/rust/comments/g6pkxn/private_types_in_public_interface/, https://internals.rust-lang.org/t/private-struct-returned-by-public-fn/8448), and some even think this should be considered a bug by the compiler.
This is my use case (simplified), in which I need to access information of the underlying Server object, for example the local address:
fn create_server() -> Graceful {
// binding to port 0 lets the OS find a random unused port for me,
// which I can then only retrieve using `server.local_addr()`
let server = Server::bind(SocketAddr::from(([127, 0, 0, 1], 0))).serve(make_svc);
// other setup stuff
// this consumes server
let graceful = server.with_graceful_shutdown(shutdown_signal());
graceful
}
#[tokio::main]
async fn main() {
let graceful = create_server();
// This doesn't compile, but I need to retrieve the port the server is running on,
// which is why I would like to be able to do something along those lines
println!("Server is running on {}", graceful.server.local_addr());
if let Err(e) = graceful.await {
eprintln!("server error: {}", e);
}
}
I know I could theoretically extract the local address before creating the Graceful, and return it alongside the Graceful object, but it sounds convoluted. Is there a better way ?
Also, I am trying to use a generic for now as you suggested, by typing the return value of my factory function as T: Future<Output = hyper::Result<()>>, but I am still getting type errors.
note: while checking the return type of the `async fn`
note: expected type parameter `T`
found struct `hyper::server::shutdown::Graceful<AddrIncoming, RouterService<Body, hyper::http::Error>, impl futures::Future, hyper::common::exec::Exec>`
EDIT: I have found a way to make this work on an very stripped-down example using the impl Trait feature, but I am still not sure of all the intricacies, as it doesn't seem to work on my actual project. I may follow up with another example if I still can't wrap my head around this.
I've experimented a bit with generics in place of Graceful, and it is not terribly satisfying. In my use case, I need to store the server instance in a struct (called Runtime in this example), along with other objects and metadata. Here is the problem when I try to use generics:
#[tokio::main]
async fn main() {
let runtime = Runtime::new(); // ERROR: type inside `async` block must be known in this context. cannot infer type for type parameter `T`.
let graceful = runtime.graceful_server;
println!("Server running on localhost:{}", runtime.addr);
match graceful.await {
Ok(()) => println!("Gracefully shutting down."),
Err(e) => println!("Server error: {}", e),
}
}
pub struct Runtime<T: Future<Output = hyper::Result<()>>> {
pub graceful_server: T,
pub addr: SocketAddr,
}
impl<T: Future<Output = hyper::Result<()>>> Runtime<T> {
pub fn new() -> Runtime<impl Future<Output = hyper::Result<()>>> {
let addr = SocketAddr::from(([127, 0, 0, 1], 0));
let make_svc = make_service_fn(|_conn| async { Ok::<_, Infallible>(service_fn(handle)) });
let server = Server::bind(&addr).serve(make_svc);
let addr = server.local_addr();
let graceful = server.with_graceful_shutdown(shutdown_signal());
Runtime {
graceful_server: graceful,
addr,
}
}
}
Rust cannot infer the type of T at the call site, thus cannot properly monomorphize the Runtime::new function. This could be solved if we were able to specify the type using Runtime::<Graceful>::new(), but Graceful being private disallows this.
There are two possible workarounds:
- Store the graceful as a
Box<dyn Future<Output = hyper::Result<()>>>, removing the need for a generic, but complexifying the typing (I even need to change the type toPin<Box<dyn Future<Output = hyper::Result<()>> + Send>>because I am spawning the server in its own task in my actual code), and losing static dispatch.
pub struct Runtime {
pub graceful_server: Pin<Box<dyn Future<Output = hyper::Result<()>> + Send>>,
pub addr: SocketAddr,
}
impl Runtime {
pub fn new() -> Runtime {
let addr = SocketAddr::from(([127, 0, 0, 1], 0));
let make_svc = make_service_fn(|_conn| async { Ok::<_, Infallible>(service_fn(handle)) });
let server = Server::bind(&addr).serve(make_svc);
let addr = server.local_addr();
let graceful = server.with_graceful_shutdown(shutdown_signal());
Runtime {
graceful_server: Box::pin(graceful),
addr,
}
}
}
- Moving the factory function out of the
implblock, but that changes the interface of my code:
pub struct Runtime<T: Future<Output = hyper::Result<()>>> {
pub graceful_server: T,
pub addr: SocketAddr,
}
pub fn new_runtime() -> Runtime<impl Future<Output = hyper::Result<()>>> {
let addr = SocketAddr::from(([127, 0, 0, 1], 0));
let make_svc = make_service_fn(|_conn| async { Ok::<_, Infallible>(service_fn(handle)) });
let server = Server::bind(&addr).serve(make_svc);
let addr = server.local_addr();
let graceful = server.with_graceful_shutdown(shutdown_signal());
Runtime {
graceful_server: graceful,
addr,
}
}
Both of these options technically work, but feel like ugly hacks to circumvent the fact that Graceful is private. I don't doubt there must be good reasons to this, but I would love if you could elaborate a bit more on that choice ?
@seanmonstar could you elaborate a bit more on why this choice was made? It requires some ugly work around such as server: Pin<Box<dyn Future<Output = hyper::Result<()>> + Send + 'static>> and was just curious as to why this is