Skip to content

Commit 2a33cb5

Browse files
committed
feat(http1): graceful shutdown first byte timeout
This commit introduces a new `Connection::graceful_shutdown_with_config` method that gives users control over the HTTP/1 graceful process. Before this commit, if a graceful shutdown was initiated on an inactive connection hyper would immediately close it. As of this commit the `GracefulShutdownConfig::first_byte_read_timeout` method can be used to give inactive connections a grace period where, should the server begin receiving bytes from them before the deadline, the request will be processed. ## HTTP/2 Graceful Shutdowns This commit does not modify hyper's HTTP/2 graceful shutdown process. `hyper` already uses the HTTP/2 `GOAWAY` frame, meaning that `hyper` already gives inactive connections a brief period during which they can transmit their final requests. Note that while this commit enables slightly more graceful shutdowns for HTTP/1 servers, HTTP/2 graceful shutdowns are still superior. HTTP/2's `GOAWAY` frame allows the server to finish processing a last batch of multiple incoming requests from the client, whereas the new graceful shutdown configuration in this commit only allows the server to wait for one final incoming request to be received. This limitations stems from a limitation in HTTP/1, where there is nothing like the `GOAWAY` frame that can be used to coordinate the graceful shutdown process with the client in the face of multiple concurrent incoming requests. Instead for HTTP/1 connections `hyper` gracefully shuts down by disabling Keep-Alive, at which point the server will only receive at most one new request, even if the client has multiple requests that are moments from reaching the server. ## Motivating Use Case I'm working on a server that is being designed to handle a large amount of traffic from a large number of clients. It is expected that many clients will open many TCP connections with the server every second. As a server receives more traffic it becomes increasingly likely that at the point that it begins gracefully shutting down there are connections that were just opened, but the client's bytes have not yet been seen by the server. Before this commit, calling `Connection::graceful_shutdown` on such a freshly opened HTTP/1 connection will immediately close it. This means that the client will get an error, despite the server having been perfectly capable of handling a final request before closing the connection. This commit solves this problem for HTTP/1 clients that tend to send one request at a time. By setting a `GracefulShutdownConfig::first_byte_read_timeout` of, say, 1 second, the server will wait a moment to see if any of the client's bytes have been received. During this period, the client will have been informed that Keep-Alive is now disabled, meaning that at most one more request will be processed. Clients that have multiple in-flight requests that have not yet reached the server will have at most one of those requests handled, even if all of them reach the server before the `first_byte_read_timeout`. This is a limitation of HTTP/1. ## Work to do in other Crates #### hyper-util To expose this to users that use `hyper-util`, a method should be added to `hyper-util`'s `Connection` type. This new `hyper-util Connection::graceful_shutdown_with_config` method would expose a `http1_first_byte_read_timeout` method that would lead `hyper-util` to set `hyper GracefulShutdownConfig::first_byte_read_timeout`. --- Closes hyperium#3792
1 parent 7f4a682 commit 2a33cb5

File tree

4 files changed

+295
-39
lines changed

4 files changed

+295
-39
lines changed

src/proto/h1/conn.rs

+98-26
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,9 @@ where
5959
h1_parser_config: ParserConfig::default(),
6060
h1_max_headers: None,
6161
#[cfg(feature = "server")]
62-
h1_header_read_timeout: None,
62+
h1_header_read_timeout: TimeoutState::default(),
6363
#[cfg(feature = "server")]
64-
h1_header_read_timeout_fut: None,
65-
#[cfg(feature = "server")]
66-
h1_header_read_timeout_running: false,
64+
h1_graceful_shutdown_first_byte_read_timeout: TimeoutState::default(),
6765
#[cfg(feature = "server")]
6866
date_header: true,
6967
#[cfg(feature = "server")]
@@ -144,7 +142,14 @@ where
144142

145143
#[cfg(feature = "server")]
146144
pub(crate) fn set_http1_header_read_timeout(&mut self, val: Duration) {
147-
self.state.h1_header_read_timeout = Some(val);
145+
self.state.h1_header_read_timeout.timeout = Some(val);
146+
}
147+
148+
#[cfg(feature = "server")]
149+
pub(crate) fn set_http1_graceful_shutdown_first_byte_read_timeout(&mut self, val: Duration) {
150+
self.state
151+
.h1_graceful_shutdown_first_byte_read_timeout
152+
.timeout = Some(val);
148153
}
149154

150155
#[cfg(feature = "server")]
@@ -209,6 +214,19 @@ where
209214
read_buf.len() >= 24 && read_buf[..24] == *H2_PREFACE
210215
}
211216

217+
fn close_if_inactive(&mut self) {
218+
// When a graceful shutdown is triggered we wait for up to some
219+
// `Duration` to allow for the client to begin transmitting bytes to the
220+
// server.
221+
// If that duration has elapsed and the connection is still idle, or
222+
// no bytes have been received on the connection, then we close it.
223+
// This prevents inactive connections from keeping the server alive
224+
// despite having no intention of sending a request.
225+
if self.is_idle() || self.has_initial_read_write_state() {
226+
self.state.close();
227+
}
228+
}
229+
212230
pub(super) fn poll_read_head(
213231
&mut self,
214232
cx: &mut Context<'_>,
@@ -217,24 +235,50 @@ where
217235
trace!("Conn::read_head");
218236

219237
#[cfg(feature = "server")]
220-
if !self.state.h1_header_read_timeout_running {
221-
if let Some(h1_header_read_timeout) = self.state.h1_header_read_timeout {
238+
if !self.state.h1_header_read_timeout.is_running {
239+
if let Some(h1_header_read_timeout) = self.state.h1_header_read_timeout.timeout {
240+
self.state.h1_header_read_timeout.is_running = true;
222241
let deadline = Instant::now() + h1_header_read_timeout;
223-
self.state.h1_header_read_timeout_running = true;
224-
match self.state.h1_header_read_timeout_fut {
242+
match self.state.h1_header_read_timeout.deadline_fut {
225243
Some(ref mut h1_header_read_timeout_fut) => {
226244
trace!("resetting h1 header read timeout timer");
227245
self.state.timer.reset(h1_header_read_timeout_fut, deadline);
228246
}
229247
None => {
230248
trace!("setting h1 header read timeout timer");
231-
self.state.h1_header_read_timeout_fut =
249+
self.state.h1_header_read_timeout.deadline_fut =
232250
Some(self.state.timer.sleep_until(deadline));
233251
}
234252
}
235253
}
236254
}
237255

256+
#[cfg(feature = "server")]
257+
if !self
258+
.state
259+
.h1_graceful_shutdown_first_byte_read_timeout
260+
.is_running
261+
{
262+
if let Some(h1_graceful_shutdown_timeout) = self
263+
.state
264+
.h1_graceful_shutdown_first_byte_read_timeout
265+
.timeout
266+
{
267+
if h1_graceful_shutdown_timeout == Duration::from_secs(0) {
268+
self.close_if_inactive();
269+
} else {
270+
self.state
271+
.h1_graceful_shutdown_first_byte_read_timeout
272+
.is_running = true;
273+
274+
let deadline = Instant::now() + h1_graceful_shutdown_timeout;
275+
self.state
276+
.h1_graceful_shutdown_first_byte_read_timeout
277+
.deadline_fut = Some(self.state.timer.sleep_until(deadline));
278+
}
279+
}
280+
}
281+
238282
let msg = match self.io.parse::<T>(
239283
cx,
240284
ParseContext {
@@ -254,27 +298,47 @@ where
254298
Poll::Ready(Err(e)) => return self.on_read_head_error(e),
255299
Poll::Pending => {
256300
#[cfg(feature = "server")]
257-
if self.state.h1_header_read_timeout_running {
301+
if self.state.h1_header_read_timeout.is_running {
258302
if let Some(ref mut h1_header_read_timeout_fut) =
259-
self.state.h1_header_read_timeout_fut
303+
self.state.h1_header_read_timeout.deadline_fut
260304
{
261305
if Pin::new(h1_header_read_timeout_fut).poll(cx).is_ready() {
262-
self.state.h1_header_read_timeout_running = false;
306+
self.state.h1_header_read_timeout.is_running = false;
263307

264308
warn!("read header from client timeout");
265309
return Poll::Ready(Some(Err(crate::Error::new_header_timeout())));
266310
}
267311
}
268312
}
269313

314+
#[cfg(feature = "server")]
315+
if self
316+
.state
317+
.h1_graceful_shutdown_first_byte_read_timeout
318+
.is_running
319+
{
320+
if let Some(ref mut h1_graceful_shutdown_timeout_fut) = self
321+
.state
322+
.h1_graceful_shutdown_first_byte_read_timeout
323+
.deadline_fut
324+
{
325+
if Pin::new(h1_graceful_shutdown_timeout_fut)
326+
.poll(cx)
327+
.is_ready()
328+
{
329+
self.close_if_inactive();
330+
}
331+
}
332+
}
333+
270334
return Poll::Pending;
271335
}
272336
};
273337

274338
#[cfg(feature = "server")]
275339
{
276-
self.state.h1_header_read_timeout_running = false;
277-
self.state.h1_header_read_timeout_fut = None;
340+
self.state.h1_header_read_timeout.is_running = false;
341+
self.state.h1_header_read_timeout.deadline_fut = None;
278342
}
279343

280344
// Note: don't deconstruct `msg` into local variables, it appears
@@ -872,15 +936,15 @@ where
872936
self.state.close_write();
873937
}
874938

939+
#[cfg(feature = "server")]
940+
pub(crate) fn is_idle(&mut self) -> bool {
941+
self.state.is_idle()
942+
}
943+
875944
#[cfg(feature = "server")]
876945
pub(crate) fn disable_keep_alive(&mut self) {
877-
if self.state.is_idle() {
878-
trace!("disable_keep_alive; closing idle connection");
879-
self.state.close();
880-
} else {
881-
trace!("disable_keep_alive; in-progress connection");
882-
self.state.disable_keep_alive();
883-
}
946+
trace!("disable_keep_alive");
947+
self.state.disable_keep_alive();
884948
}
885949

886950
pub(crate) fn take_error(&mut self) -> crate::Result<()> {
@@ -926,11 +990,11 @@ struct State {
926990
h1_parser_config: ParserConfig,
927991
h1_max_headers: Option<usize>,
928992
#[cfg(feature = "server")]
929-
h1_header_read_timeout: Option<Duration>,
993+
h1_header_read_timeout: TimeoutState,
994+
/// If a graceful shutdown is initiated, and the `TimeoutState` duration has elapsed without
995+
/// receiving any bytes from the client, the connection will be closed.
930996
#[cfg(feature = "server")]
931-
h1_header_read_timeout_fut: Option<Pin<Box<dyn Sleep>>>,
932-
#[cfg(feature = "server")]
933-
h1_header_read_timeout_running: bool,
997+
h1_graceful_shutdown_first_byte_read_timeout: TimeoutState,
934998
#[cfg(feature = "server")]
935999
date_header: bool,
9361000
#[cfg(feature = "server")]
@@ -1144,6 +1208,14 @@ impl State {
11441208
}
11451209
}
11461210

1211+
#[derive(Default)]
1212+
#[cfg(feature = "server")]
1213+
struct TimeoutState {
1214+
timeout: Option<Duration>,
1215+
deadline_fut: Option<Pin<Box<dyn Sleep>>>,
1216+
is_running: bool,
1217+
}
1218+
11471219
#[cfg(test)]
11481220
mod tests {
11491221
#[cfg(all(feature = "nightly", not(miri)))]

src/proto/h1/dispatch.rs

+10-11
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
use crate::rt::{Read, Write};
2+
use bytes::{Buf, Bytes};
3+
use futures_util::ready;
4+
use http::Request;
5+
use std::time::Duration;
16
use std::{
27
error::Error as StdError,
38
future::Future,
@@ -6,11 +11,6 @@ use std::{
611
task::{Context, Poll},
712
};
813

9-
use crate::rt::{Read, Write};
10-
use bytes::{Buf, Bytes};
11-
use futures_util::ready;
12-
use http::Request;
13-
1414
use super::{Http1Transaction, Wants};
1515
use crate::body::{Body, DecodedLength, Incoming as IncomingBody};
1616
#[cfg(feature = "client")]
@@ -90,13 +90,12 @@ where
9090
#[cfg(feature = "server")]
9191
pub(crate) fn disable_keep_alive(&mut self) {
9292
self.conn.disable_keep_alive();
93+
}
9394

94-
// If keep alive has been disabled and no read or write has been seen on
95-
// the connection yet, we must be in a state where the server is being asked to
96-
// shut down before any data has been seen on the connection
97-
if self.conn.is_write_closed() || self.conn.has_initial_read_write_state() {
98-
self.close();
99-
}
95+
#[cfg(feature = "server")]
96+
pub(crate) fn set_graceful_shutdown_first_byte_read_timeout(&mut self, read_timeout: Duration) {
97+
self.conn
98+
.set_http1_graceful_shutdown_first_byte_read_timeout(read_timeout);
10099
}
101100

102101
pub(crate) fn into_inner(self) -> (I, Bytes, D) {

src/server/conn/http1.rs

+94-2
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,8 @@ where
123123
B: Body + 'static,
124124
B::Error: Into<Box<dyn StdError + Send + Sync>>,
125125
{
126-
/// Start a graceful shutdown process for this connection.
126+
/// Start a graceful shutdown process for this connection, using the default
127+
/// [`GracefulShutdownConfig`].
127128
///
128129
/// This `Connection` should continue to be polled until shutdown
129130
/// can finish.
@@ -133,8 +134,29 @@ where
133134
/// This should only be called while the `Connection` future is still
134135
/// pending. If called after `Connection::poll` has resolved, this does
135136
/// nothing.
136-
pub fn graceful_shutdown(mut self: Pin<&mut Self>) {
137+
pub fn graceful_shutdown(self: Pin<&mut Self>) {
138+
self.graceful_shutdown_with_config(GracefulShutdownConfig::default());
139+
}
140+
141+
/// Start a graceful shutdown process for this connection.
142+
///
143+
/// This `Connection` should continue to be polled until shutdown can finish.
144+
///
145+
/// Requires a [`Timer`] set by [`Builder::timer`].
146+
///
147+
/// # Note
148+
///
149+
/// This should only be called while the `Connection` future is still
150+
/// pending. If called after `Connection::poll` has resolved, this does
151+
/// nothing.
152+
///
153+
/// # Panics
154+
/// If [`GracefulShutdownConfig::first_byte_read_timeout`] was configured to greater than zero
155+
/// nanoseconds, but no timer was set, then the `Connection` will panic when it is next polled.
156+
pub fn graceful_shutdown_with_config(mut self: Pin<&mut Self>, config: GracefulShutdownConfig) {
137157
self.conn.disable_keep_alive();
158+
self.conn
159+
.set_graceful_shutdown_first_byte_read_timeout(config.first_byte_read_timeout);
138160
}
139161

140162
/// Return the inner IO object, and additional information.
@@ -526,3 +548,73 @@ where
526548
}
527549
}
528550
}
551+
552+
/// Configuration for graceful shutdowns.
553+
///
554+
/// # Example
555+
///
556+
/// ```
557+
/// # use hyper::{body::Incoming, Request, Response};
558+
/// # use hyper::service::Service;
559+
/// # use hyper::server::conn::http1::Builder;
560+
/// # use hyper::rt::{Read, Write};
561+
/// # use std::time::Duration;
562+
/// # use hyper::server::conn::http1::GracefulShutdownConfig;
563+
/// # async fn run<I, S>(some_io: I, some_service: S)
564+
/// # where
565+
/// # I: Read + Write + Unpin + Send + 'static,
566+
/// # S: Service<hyper::Request<Incoming>, Response=hyper::Response<Incoming>> + Send + 'static,
567+
/// # S::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
568+
/// # S::Future: Send,
569+
/// # {
570+
/// let http = Builder::new();
571+
/// let conn = http.serve_connection(some_io, some_service);
572+
///
573+
/// let mut config = GracefulShutdownConfig::default();
574+
/// config.first_byte_read_timeout(Duration::from_secs(2));
575+
///
576+
/// conn.graceful_shutdown_with_config(config);
577+
/// conn.await.unwrap();
578+
/// # }
579+
/// # fn main() {}
580+
/// ```
581+
#[derive(Debug)]
582+
pub struct GracefulShutdownConfig {
583+
first_byte_read_timeout: Duration,
584+
}
585+
impl Default for GracefulShutdownConfig {
586+
fn default() -> Self {
587+
GracefulShutdownConfig {
588+
first_byte_read_timeout: Duration::from_secs(0),
589+
}
590+
}
591+
}
592+
impl GracefulShutdownConfig {
593+
/// It is possible for a client to open a connection and begin transmitting bytes, but have the
594+
/// server initiate a graceful shutdown just before it sees any of the client's bytes.
595+
///
596+
/// The more traffic that a server receives, the more likely this race condition is to occur for
597+
/// some of the open connections.
598+
///
599+
/// The `first_byte_read_timeout` controls how long the server waits for the first bytes of a
600+
/// final request to be received from the client.
601+
///
602+
/// If no bytes were received from the client between the time that keep alive was disabled and
603+
/// the `first_byte_timeout` duration, the connection is considered inactive and the server will
604+
/// close it.
605+
///
606+
/// # Recommendations
607+
/// Servers are recommended to use a `first_byte_read_timeout` that reduces the likelihood of
608+
/// the client receiving an error due to the connection closing just after they began
609+
/// transmitting their final request.
610+
/// For most internet connections, a roughly one second timeout should be enough time for the
611+
/// server to begin receiving the client's request's bytes.
612+
///
613+
/// # Default
614+
/// A default of 0 seconds was chosen to remain backwards compatible with version of hyper that
615+
/// did not have this `first_byte_read_timeout` configuration.
616+
pub fn first_byte_read_timeout(&mut self, timeout: Duration) -> &mut Self {
617+
self.first_byte_read_timeout = timeout;
618+
self
619+
}
620+
}

0 commit comments

Comments
 (0)