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

Connection::graceful_shutdown always waits for the first request. #2730

Closed
sfackler opened this issue Jan 1, 2022 · 10 comments · Fixed by #3261
Closed

Connection::graceful_shutdown always waits for the first request. #2730

sfackler opened this issue Jan 1, 2022 · 10 comments · Fixed by #3261
Labels
A-server Area: server. B-breaking-change Blocked: this is an "API breaking change". C-bug Category: bug. Something is wrong. This is bad! E-medium Effort: medium. Some knowledge of how hyper internal works would be useful.
Milestone

Comments

@sfackler
Copy link
Contributor

sfackler commented Jan 1, 2022

Version
hyper 0.14.16

Platform
Linux DESKTOP-DHO88R7 4.19.104-microsoft-standard #1 SMP Wed Feb 19 06:37:35 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux

Description
If you gracefully shut down a server connection future before the first request, Hyper will not actually shut the connection down until a request is processed:

use hyper::server::conn::Http;
use hyper::Response;
use tokio::io::AsyncReadExt;
use tokio::net::{TcpListener, TcpStream};
use tokio::pin;

#[tokio::main]
async fn main() {
    let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
    let addr = listener.local_addr().unwrap();

    tokio::spawn(server(listener));

    let mut stream = TcpStream::connect(addr).await.unwrap();
    println!("connected");
    let mut buf = vec![];
    stream.read_to_end(&mut buf).await.unwrap();
}

async fn server(listener: TcpListener) {
    let socket = listener.accept().await.unwrap().0;

    let service = hyper::service::service_fn(|_: hyper::Request<hyper::Body>| async {
        Err::<Response<hyper::Body>, _>("no")
    });

    let future = Http::new()
        .http1_only(true)
        .serve_connection(socket, service);
    pin!(future);
    future.as_mut().graceful_shutdown();

    future.await.unwrap();
}

I would expect this program to exit almost instantly since there is no request being processed when the graceful_shutdown is invoked. However, it instead blocks forever waiting on the client to send headers.

The behavior actually appears to be that the shutdown is processed immediately after the first request is fully handled.

@sfackler sfackler added the C-bug Category: bug. Something is wrong. This is bad! label Jan 1, 2022
@seanmonstar
Copy link
Member

Yea, I think this is behavior is currently on purpose. Whether it should be is a fair question. I think at the time, I assumed that a new connection would usually have a request incoming ASAP, whereas an idle connection might not, and so it was better to allow that request to come in.

We could alter that behavior, if we document a good reason for why we're making that change. Also, I imagine using the http1_header_read_timeout option could help here.

@seanmonstar seanmonstar added the B-rfc Blocked: More comments would be useful in determine next steps. label Jan 3, 2022
@sfackler
Copy link
Contributor Author

sfackler commented Jan 3, 2022

I agree that it's more likely for the first request to show up than a follow up request, but it seems weird to bake that assumption into graceful_shutdown IMO. For reference, I'm using graceful_shutdown in some logic to shut down connections after a certain period of idleness (I can't just terminate the connection future since I want the TLS session to shut down cleanly for session reuse). Right now, the current behavior will miss connections that are opened and never make a request.

Does http1_header_read_timeout start counting immediately or just once the first byte of the header is received? In any case, I don't think that'd help for an h2 connection, right?

@sawyersteven
Copy link

Any progress on this? I was trying to figure out why my program wasn't exiting when I expected it to and found this issue. If I send the graceful shutdown signal the server will process one more request before actually exiting. So a user sending the shutdown command through a web app won't actually be shutting anything down.

@seanmonstar seanmonstar moved this to Needs discussion / design in hyper 1.0 Aug 10, 2022
@seanmonstar seanmonstar added this to the 1.0 milestone Aug 10, 2022
@seanmonstar seanmonstar modified the milestones: 1.0 RC1, 1.0 Final Aug 22, 2022
@bossmc
Copy link
Contributor

bossmc commented Nov 3, 2022

Does http1_header_read_timeout start counting immediately or just once the first byte of the header is received? In any case, I don't think that'd help for an h2 connection, right?

From some testing just now, the answer to the first part is "no". If I start-up an http1::Connection with a header timeout configured and connect to it simply with nc then the connection stays up indefinitely. Once I send any data (even a blank line) the timer starts counting and the connection is closed as expected after the timeout.

@seanmonstar
Copy link
Member

This seems like a thing to slide into RC2 or RC3.

I can't just terminate the connection future since I want the TLS session to shut down cleanly for session reuse

@sfackler Does this mean you'd have the connection write out a 408 response and then finish? Or simply calling poll_shutdown and then return the future like if it were in keep-alive state? I guess calling shutdown will make the TLS shutdown start, right?

@sfackler
Copy link
Contributor Author

Yeah it just needs to call shutdown on the underlying IO object, which will handle the SSL shutdown protocol.

@seanmonstar seanmonstar moved this from Needs discussion / design to Blocked in hyper 1.0 Dec 21, 2022
@seanmonstar seanmonstar added A-server Area: server. C-feature Category: feature. This is adding a new feature. E-medium Effort: medium. Some knowledge of how hyper internal works would be useful. B-breaking-change Blocked: this is an "API breaking change". and removed B-rfc Blocked: More comments would be useful in determine next steps. C-feature Category: feature. This is adding a new feature. labels Dec 23, 2022
@seanmonstar seanmonstar moved this from Blocked to Todo in hyper 1.0 Dec 23, 2022
@seanmonstar seanmonstar modified the milestones: 1.0 Final, 1.0 RC4 Feb 28, 2023
@rob2244
Copy link
Contributor

rob2244 commented May 13, 2023

I think I can take this one.

KotaHv added a commit to KotaHv/mikan-proxy that referenced this issue May 15, 2023
@seanmonstar seanmonstar moved this from Todo to In Progress in hyper 1.0 May 18, 2023
@rob2244
Copy link
Contributor

rob2244 commented May 21, 2023

@seanmonstar so the issue right now is this:

if we try to control this using the keep_alive state (which is what we're currently doing) it introduces a race condition of sorts.
If keep_alive is initially Idle, when disable_keep_alive() is called (either directly or through graceful_shutdown) the connection will shut down before servicing any requests. This seems like incorrect behavior, it breaks some unit tests and I would expect that the connection would respond to one request before shutting down.

If keep_alive is initially Busy then the connection will wait forever on the read of the initial request.

I think we have two options here: either we keep the initial state as Busy and add some sort of read timeout somewhere, or we keep the initial state as Idle and do a peek on the underlying io stream and process any data found there before closing the connection.

edit:
Actually I found another way to do it I think. Let me see if it works. Not sure I'm super happy with it thought because it feels a little like catering to an edge case too much

rob2244 pushed a commit to rob2244/hyper that referenced this issue May 23, 2023
fix issue in the graceful shutdown logic
which causes the connection future to hang
when graceful shutdown is called prior to any
requests being  made. This fix checks to see
if the connection is still in its initial state
when disable_keep_alive is called, and starts the
shutdown process if it is.

This addresses issue hyperium#2730
@seanmonstar
Copy link
Member

If keep_alive is initially Idle, when disable_keep_alive() is called (either directly or through graceful_shutdown) the connection will shut down before servicing any requests.

That seems like the desired outcome of this issue, no? Or do you mean the request was already received, and the connection just aborts abruptly?

@rob2244
Copy link
Contributor

rob2244 commented May 24, 2023

@seanmonstar yeah so essentially if a http1 server conn is created with .keep_alive(false) the server shuts down without servicing any requests. Is that the behavior we want? I would think you would want the server to service one request to see if that request wants to keep the connection alive or not.

The following two tests fail when the initial keep alive state is set to idle:

image

image

The read_to_end in both cases returns an empty array because the server shut down before processing anything.

@seanmonstar seanmonstar linked a pull request Jul 6, 2023 that will close this issue
seanmonstar pushed a commit that referenced this issue Jul 6, 2023
fix issue in the graceful shutdown logic
which causes the connection future to hang
when graceful shutdown is called prior to any
requests being  made. This fix checks to see
if the connection is still in its initial state
when disable_keep_alive is called, and starts the
shutdown process if it is.

This addresses issue #2730
seanmonstar pushed a commit that referenced this issue Jul 6, 2023
fix issue in the graceful shutdown logic
which causes the connection future to hang
when graceful shutdown is called prior to any
requests being  made. This fix checks to see
if the connection is still in its initial state
when disable_keep_alive is called, and starts the
shutdown process if it is.

This addresses issue #2730
seanmonstar added a commit that referenced this issue Jul 6, 2023
fix issue in the graceful shutdown logic
which causes the connection future to hang
when graceful shutdown is called prior to any
requests being  made. This fix checks to see
if the connection is still in its initial state
when disable_keep_alive is called, and starts the
shutdown process if it is.

This addresses issue #2730

Co-authored-by: Robin Seitz <roseitz@microsoft.com>
@github-project-automation github-project-automation bot moved this from In Progress to Done in hyper 1.0 Jul 6, 2023
0xE282B0 pushed a commit to 0xE282B0/hyper that referenced this issue Jan 12, 2024
fix issue in the graceful shutdown logic
which causes the connection future to hang
when graceful shutdown is called prior to any
requests being  made. This fix checks to see
if the connection is still in its initial state
when disable_keep_alive is called, and starts the
shutdown process if it is.

This addresses issue hyperium#2730

Co-authored-by: Robin Seitz <roseitz@microsoft.com>
0xE282B0 pushed a commit to 0xE282B0/hyper that referenced this issue Jan 16, 2024
fix issue in the graceful shutdown logic
which causes the connection future to hang
when graceful shutdown is called prior to any
requests being  made. This fix checks to see
if the connection is still in its initial state
when disable_keep_alive is called, and starts the
shutdown process if it is.

This addresses issue hyperium#2730

Co-authored-by: Robin Seitz <roseitz@microsoft.com>
Signed-off-by: Sven Pfennig <s.pfennig@reply.de>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-server Area: server. B-breaking-change Blocked: this is an "API breaking change". C-bug Category: bug. Something is wrong. This is bad! E-medium Effort: medium. Some knowledge of how hyper internal works would be useful.
Projects
No open projects
Status: Done
5 participants