feat(http2): add HTTP2 keep-alive support for client and server

This adds HTTP2 keep-alive support to client and server connections
based losely on GRPC keep-alive. When enabled, after no data has been
received for some configured interval, an HTTP2 PING frame is sent. If
the PING is not acknowledged with a configured timeout, the connection
is closed.

Clients have an additional option to enable keep-alive while the
connection is otherwise idle. When disabled, keep-alive PINGs are only
used while there are open request/response streams. If enabled, PINGs
are sent even when there are no active streams.

For now, since these features use `tokio::time::Delay`, the `runtime`
cargo feature is required to use them.
This commit is contained in:
Sean McArthur
2020-03-20 13:58:52 -07:00
parent d838d54fdf
commit 9a8413d910
13 changed files with 1166 additions and 255 deletions

View File

@@ -1,12 +1,14 @@
use std::error::Error as StdError;
use std::marker::Unpin;
#[cfg(feature = "runtime")]
use std::time::Duration;
use h2::server::{Connection, Handshake, SendResponse};
use h2::Reason;
use pin_project::{pin_project, project};
use tokio::io::{AsyncRead, AsyncWrite};
use super::{bdp, decode_content_length, PipeToSendStream, SendBuf};
use super::{decode_content_length, ping, PipeToSendStream, SendBuf};
use crate::body::Payload;
use crate::common::exec::H2Exec;
use crate::common::{task, Future, Pin, Poll};
@@ -31,6 +33,10 @@ pub(crate) struct Config {
pub(crate) initial_conn_window_size: u32,
pub(crate) initial_stream_window_size: u32,
pub(crate) max_concurrent_streams: Option<u32>,
#[cfg(feature = "runtime")]
pub(crate) keep_alive_interval: Option<Duration>,
#[cfg(feature = "runtime")]
pub(crate) keep_alive_timeout: Duration,
}
impl Default for Config {
@@ -40,6 +46,10 @@ impl Default for Config {
initial_conn_window_size: DEFAULT_CONN_WINDOW,
initial_stream_window_size: DEFAULT_STREAM_WINDOW,
max_concurrent_streams: None,
#[cfg(feature = "runtime")]
keep_alive_interval: None,
#[cfg(feature = "runtime")]
keep_alive_timeout: Duration::from_secs(20),
}
}
}
@@ -60,10 +70,7 @@ where
B: Payload,
{
Handshaking {
/// If Some, bdp is enabled with the initial size.
///
/// If None, bdp is disabled.
bdp_initial_size: Option<u32>,
ping_config: ping::Config,
hs: Handshake<T, SendBuf<B::Data>>,
},
Serving(Serving<T, B>),
@@ -74,7 +81,7 @@ struct Serving<T, B>
where
B: Payload,
{
bdp: Option<(bdp::Sampler, bdp::Estimator)>,
ping: Option<(ping::Recorder, ping::Ponger)>,
conn: Connection<T, SendBuf<B::Data>>,
closing: Option<crate::Error>,
}
@@ -103,10 +110,22 @@ where
None
};
let ping_config = ping::Config {
bdp_initial_window: bdp,
#[cfg(feature = "runtime")]
keep_alive_interval: config.keep_alive_interval,
#[cfg(feature = "runtime")]
keep_alive_timeout: config.keep_alive_timeout,
// If keep-alive is enabled for servers, always enabled while
// idle, so it can more aggresively close dead connections.
#[cfg(feature = "runtime")]
keep_alive_while_idle: true,
};
Server {
exec,
state: State::Handshaking {
bdp_initial_size: bdp,
ping_config,
hs: handshake,
},
service,
@@ -149,13 +168,17 @@ where
let next = match me.state {
State::Handshaking {
ref mut hs,
ref bdp_initial_size,
ref ping_config,
} => {
let mut conn = ready!(Pin::new(hs).poll(cx).map_err(crate::Error::new_h2))?;
let bdp = bdp_initial_size
.map(|wnd| bdp::channel(conn.ping_pong().expect("ping_pong"), wnd));
let ping = if ping_config.is_enabled() {
let pp = conn.ping_pong().expect("conn.ping_pong");
Some(ping::channel(pp, ping_config.clone()))
} else {
None
};
State::Serving(Serving {
bdp,
ping,
conn,
closing: None,
})
@@ -193,7 +216,7 @@ where
{
if self.closing.is_none() {
loop {
self.poll_bdp(cx);
self.poll_ping(cx);
// Check that the service is ready to accept a new request.
//
@@ -231,14 +254,16 @@ where
Some(Ok((req, respond))) => {
trace!("incoming request");
let content_length = decode_content_length(req.headers());
let bdp_sampler = self
.bdp
let ping = self
.ping
.as_ref()
.map(|bdp| bdp.0.clone())
.unwrap_or_else(bdp::disabled);
.map(|ping| ping.0.clone())
.unwrap_or_else(ping::disabled);
let req =
req.map(|stream| crate::Body::h2(stream, content_length, bdp_sampler));
// Record the headers received
ping.record_non_data();
let req = req.map(|stream| crate::Body::h2(stream, content_length, ping));
let fut = H2Stream::new(service.call(req), respond);
exec.execute_h2stream(fut);
}
@@ -247,6 +272,10 @@ where
}
None => {
// no more incoming streams...
if let Some((ref ping, _)) = self.ping {
ping.ensure_not_timed_out()?;
}
trace!("incoming connection complete");
return Poll::Ready(Ok(()));
}
@@ -264,13 +293,18 @@ where
Poll::Ready(Err(self.closing.take().expect("polled after error")))
}
fn poll_bdp(&mut self, cx: &mut task::Context<'_>) {
if let Some((_, ref mut estimator)) = self.bdp {
match estimator.poll_estimate(cx) {
Poll::Ready(wnd) => {
fn poll_ping(&mut self, cx: &mut task::Context<'_>) {
if let Some((_, ref mut estimator)) = self.ping {
match estimator.poll(cx) {
Poll::Ready(ping::Ponged::SizeUpdate(wnd)) => {
self.conn.set_target_window_size(wnd);
let _ = self.conn.set_initial_window_size(wnd);
}
#[cfg(feature = "runtime")]
Poll::Ready(ping::Ponged::KeepAliveTimedOut) => {
debug!("keep-alive timed out, closing connection");
self.conn.abrupt_shutdown(h2::Reason::NO_ERROR);
}
Poll::Pending => {}
}
}