Skip to content

Commit 9a8413d

Browse files
committed
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.
1 parent d838d54 commit 9a8413d

File tree

13 files changed

+1166
-255
lines changed

13 files changed

+1166
-255
lines changed

src/body/body.rs

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use http::HeaderMap;
1212
use http_body::{Body as HttpBody, SizeHint};
1313

1414
use crate::common::{task, watch, Future, Never, Pin, Poll};
15-
use crate::proto::h2::bdp;
15+
use crate::proto::h2::ping;
1616
use crate::proto::DecodedLength;
1717
use crate::upgrade::OnUpgrade;
1818

@@ -38,7 +38,7 @@ enum Kind {
3838
rx: mpsc::Receiver<Result<Bytes, crate::Error>>,
3939
},
4040
H2 {
41-
bdp: bdp::Sampler,
41+
ping: ping::Recorder,
4242
content_length: DecodedLength,
4343
recv: h2::RecvStream,
4444
},
@@ -180,10 +180,10 @@ impl Body {
180180
pub(crate) fn h2(
181181
recv: h2::RecvStream,
182182
content_length: DecodedLength,
183-
bdp: bdp::Sampler,
183+
ping: ping::Recorder,
184184
) -> Self {
185185
let body = Body::new(Kind::H2 {
186-
bdp,
186+
ping,
187187
content_length,
188188
recv,
189189
});
@@ -265,14 +265,14 @@ impl Body {
265265
}
266266
}
267267
Kind::H2 {
268-
ref bdp,
268+
ref ping,
269269
recv: ref mut h2,
270270
content_length: ref mut len,
271271
} => match ready!(h2.poll_data(cx)) {
272272
Some(Ok(bytes)) => {
273273
let _ = h2.flow_control().release_capacity(bytes.len());
274274
len.sub_if(bytes.len() as u64);
275-
bdp.sample(bytes.len());
275+
ping.record_data(bytes.len());
276276
Poll::Ready(Some(Ok(bytes)))
277277
}
278278
Some(Err(e)) => Poll::Ready(Some(Err(crate::Error::new_body(e)))),
@@ -321,9 +321,14 @@ impl HttpBody for Body {
321321
) -> Poll<Result<Option<HeaderMap>, Self::Error>> {
322322
match self.kind {
323323
Kind::H2 {
324-
recv: ref mut h2, ..
324+
recv: ref mut h2,
325+
ref ping,
326+
..
325327
} => match ready!(h2.poll_trailers(cx)) {
326-
Ok(t) => Poll::Ready(Ok(t)),
328+
Ok(t) => {
329+
ping.record_non_data();
330+
Poll::Ready(Ok(t))
331+
}
327332
Err(e) => Poll::Ready(Err(crate::Error::new_h2(e))),
328333
},
329334
_ => Poll::Ready(Ok(None)),

src/client/conn.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,12 @@
77
//!
88
//! If don't have need to manage connections yourself, consider using the
99
//! higher-level [Client](super) API.
10+
1011
use std::fmt;
1112
use std::mem;
1213
use std::sync::Arc;
14+
#[cfg(feature = "runtime")]
15+
use std::time::Duration;
1316

1417
use bytes::Bytes;
1518
use futures_util::future::{self, Either, FutureExt as _};
@@ -517,6 +520,59 @@ impl Builder {
517520
self
518521
}
519522

523+
/// Sets an interval for HTTP2 Ping frames should be sent to keep a
524+
/// connection alive.
525+
///
526+
/// Pass `None` to disable HTTP2 keep-alive.
527+
///
528+
/// Default is currently disabled.
529+
///
530+
/// # Cargo Feature
531+
///
532+
/// Requires the `runtime` cargo feature to be enabled.
533+
#[cfg(feature = "runtime")]
534+
pub fn http2_keep_alive_interval(
535+
&mut self,
536+
interval: impl Into<Option<Duration>>,
537+
) -> &mut Self {
538+
self.h2_builder.keep_alive_interval = interval.into();
539+
self
540+
}
541+
542+
/// Sets a timeout for receiving an acknowledgement of the keep-alive ping.
543+
///
544+
/// If the ping is not acknowledged within the timeout, the connection will
545+
/// be closed. Does nothing if `http2_keep_alive_interval` is disabled.
546+
///
547+
/// Default is 20 seconds.
548+
///
549+
/// # Cargo Feature
550+
///
551+
/// Requires the `runtime` cargo feature to be enabled.
552+
#[cfg(feature = "runtime")]
553+
pub fn http2_keep_alive_timeout(&mut self, timeout: Duration) -> &mut Self {
554+
self.h2_builder.keep_alive_timeout = timeout;
555+
self
556+
}
557+
558+
/// Sets whether HTTP2 keep-alive should apply while the connection is idle.
559+
///
560+
/// If disabled, keep-alive pings are only sent while there are open
561+
/// request/responses streams. If enabled, pings are also sent when no
562+
/// streams are active. Does nothing if `http2_keep_alive_interval` is
563+
/// disabled.
564+
///
565+
/// Default is `false`.
566+
///
567+
/// # Cargo Feature
568+
///
569+
/// Requires the `runtime` cargo feature to be enabled.
570+
#[cfg(feature = "runtime")]
571+
pub fn http2_keep_alive_while_idle(&mut self, enabled: bool) -> &mut Self {
572+
self.h2_builder.keep_alive_while_idle = enabled;
573+
self
574+
}
575+
520576
/// Constructs a connection with the configured options and IO.
521577
pub fn handshake<T, B>(
522578
&self,

src/client/mod.rs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -933,6 +933,7 @@ impl Builder {
933933
self.pool_config.max_idle_per_host = max_idle;
934934
self
935935
}
936+
936937
// HTTP/1 options
937938

938939
/// Set whether HTTP/1 connections should try to use vectored writes,
@@ -1036,6 +1037,59 @@ impl Builder {
10361037
self
10371038
}
10381039

1040+
/// Sets an interval for HTTP2 Ping frames should be sent to keep a
1041+
/// connection alive.
1042+
///
1043+
/// Pass `None` to disable HTTP2 keep-alive.
1044+
///
1045+
/// Default is currently disabled.
1046+
///
1047+
/// # Cargo Feature
1048+
///
1049+
/// Requires the `runtime` cargo feature to be enabled.
1050+
#[cfg(feature = "runtime")]
1051+
pub fn http2_keep_alive_interval(
1052+
&mut self,
1053+
interval: impl Into<Option<Duration>>,
1054+
) -> &mut Self {
1055+
self.conn_builder.http2_keep_alive_interval(interval);
1056+
self
1057+
}
1058+
1059+
/// Sets a timeout for receiving an acknowledgement of the keep-alive ping.
1060+
///
1061+
/// If the ping is not acknowledged within the timeout, the connection will
1062+
/// be closed. Does nothing if `http2_keep_alive_interval` is disabled.
1063+
///
1064+
/// Default is 20 seconds.
1065+
///
1066+
/// # Cargo Feature
1067+
///
1068+
/// Requires the `runtime` cargo feature to be enabled.
1069+
#[cfg(feature = "runtime")]
1070+
pub fn http2_keep_alive_timeout(&mut self, timeout: Duration) -> &mut Self {
1071+
self.conn_builder.http2_keep_alive_timeout(timeout);
1072+
self
1073+
}
1074+
1075+
/// Sets whether HTTP2 keep-alive should apply while the connection is idle.
1076+
///
1077+
/// If disabled, keep-alive pings are only sent while there are open
1078+
/// request/responses streams. If enabled, pings are also sent when no
1079+
/// streams are active. Does nothing if `http2_keep_alive_interval` is
1080+
/// disabled.
1081+
///
1082+
/// Default is `false`.
1083+
///
1084+
/// # Cargo Feature
1085+
///
1086+
/// Requires the `runtime` cargo feature to be enabled.
1087+
#[cfg(feature = "runtime")]
1088+
pub fn http2_keep_alive_while_idle(&mut self, enabled: bool) -> &mut Self {
1089+
self.conn_builder.http2_keep_alive_while_idle(enabled);
1090+
self
1091+
}
1092+
10391093
/// Set whether to retry requests that get disrupted before ever starting
10401094
/// to write.
10411095
///

src/error.rs

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,10 @@ pub(crate) enum User {
9191
ManualUpgrade,
9292
}
9393

94+
// Sentinel type to indicate the error was caused by a timeout.
95+
#[derive(Debug)]
96+
pub(crate) struct TimedOut;
97+
9498
impl Error {
9599
/// Returns true if this was an HTTP parse error.
96100
pub fn is_parse(&self) -> bool {
@@ -133,6 +137,11 @@ impl Error {
133137
self.inner.kind == Kind::BodyWriteAborted
134138
}
135139

140+
/// Returns true if the error was caused by a timeout.
141+
pub fn is_timeout(&self) -> bool {
142+
self.find_source::<TimedOut>().is_some()
143+
}
144+
136145
/// Consumes the error, returning its cause.
137146
pub fn into_cause(self) -> Option<Box<dyn StdError + Send + Sync>> {
138147
self.inner.cause
@@ -153,19 +162,25 @@ impl Error {
153162
&self.inner.kind
154163
}
155164

156-
pub(crate) fn h2_reason(&self) -> h2::Reason {
157-
// Find an h2::Reason somewhere in the cause stack, if it exists,
158-
// otherwise assume an INTERNAL_ERROR.
165+
fn find_source<E: StdError + 'static>(&self) -> Option<&E> {
159166
let mut cause = self.source();
160167
while let Some(err) = cause {
161-
if let Some(h2_err) = err.downcast_ref::<h2::Error>() {
162-
return h2_err.reason().unwrap_or(h2::Reason::INTERNAL_ERROR);
168+
if let Some(ref typed) = err.downcast_ref() {
169+
return Some(typed);
163170
}
164171
cause = err.source();
165172
}
166173

167174
// else
168-
h2::Reason::INTERNAL_ERROR
175+
None
176+
}
177+
178+
pub(crate) fn h2_reason(&self) -> h2::Reason {
179+
// Find an h2::Reason somewhere in the cause stack, if it exists,
180+
// otherwise assume an INTERNAL_ERROR.
181+
self.find_source::<h2::Error>()
182+
.and_then(|h2_err| h2_err.reason())
183+
.unwrap_or(h2::Reason::INTERNAL_ERROR)
169184
}
170185

171186
pub(crate) fn new_canceled() -> Error {
@@ -397,6 +412,16 @@ trait AssertSendSync: Send + Sync + 'static {}
397412
#[doc(hidden)]
398413
impl AssertSendSync for Error {}
399414

415+
// ===== impl TimedOut ====
416+
417+
impl fmt::Display for TimedOut {
418+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
419+
f.write_str("operation timed out")
420+
}
421+
}
422+
423+
impl StdError for TimedOut {}
424+
400425
#[cfg(test)]
401426
mod tests {
402427
use super::*;

0 commit comments

Comments
 (0)