Skip to content

Commit 71b511a

Browse files
committed
chore(http): Add a TimeoutBody middleware
This commit implements a new TimeoutBody middleware that uses the new `Body::poll_progess` method to constrain the amount of time a stream waits for send capacity. This change does not yet wire up the middleware into the Linkerd server.
1 parent ad2917e commit 71b511a

4 files changed

Lines changed: 217 additions & 0 deletions

File tree

Cargo.lock

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -640,6 +640,17 @@ version = "0.3.30"
640640
source = "registry+https://github.com/rust-lang/crates.io-index"
641641
checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
642642

643+
[[package]]
644+
name = "futures-macro"
645+
version = "0.3.30"
646+
source = "registry+https://github.com/rust-lang/crates.io-index"
647+
checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
648+
dependencies = [
649+
"proc-macro2",
650+
"quote",
651+
"syn",
652+
]
653+
643654
[[package]]
644655
name = "futures-sink"
645656
version = "0.3.30"
@@ -661,6 +672,7 @@ dependencies = [
661672
"futures-channel",
662673
"futures-core",
663674
"futures-io",
675+
"futures-macro",
664676
"futures-sink",
665677
"futures-task",
666678
"memchr",
@@ -1444,6 +1456,20 @@ dependencies = [
14441456
"tracing",
14451457
]
14461458

1459+
[[package]]
1460+
name = "linkerd-http-body-timeout"
1461+
version = "0.1.0"
1462+
dependencies = [
1463+
"futures",
1464+
"http",
1465+
"http-body",
1466+
"linkerd-error",
1467+
"pin-project",
1468+
"thiserror",
1469+
"tokio",
1470+
"tower-service",
1471+
]
1472+
14471473
[[package]]
14481474
name = "linkerd-http-box"
14491475
version = "0.1.0"

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ members = [
2525
"linkerd/error-respond",
2626
"linkerd/exp-backoff",
2727
"linkerd/http/access-log",
28+
"linkerd/http/body-timeout",
2829
"linkerd/http/box",
2930
"linkerd/http/classify",
3031
"linkerd/http/metrics",
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
[package]
2+
name = "linkerd-http-body-timeout"
3+
version = "0.1.0"
4+
edition = "2021"
5+
publish = false
6+
7+
[dependencies]
8+
http = "0.2"
9+
http-body = "0.4"
10+
futures = "0.3"
11+
pin-project = "1"
12+
thiserror = "1"
13+
tokio = { version = "1", features = ["time"] }
14+
tower-service = "0.3"
15+
16+
linkerd-error = { path = "../../error" }
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
#![deny(rust_2018_idioms, clippy::disallowed_methods, clippy::disallowed_types)]
2+
#![forbid(unsafe_code)]
3+
4+
use futures::prelude::*;
5+
use http::{HeaderMap, HeaderValue};
6+
use http_body::Body;
7+
use linkerd_error::Error;
8+
use pin_project::pin_project;
9+
use std::{
10+
future::Future,
11+
pin::Pin,
12+
task::{Context, Poll},
13+
};
14+
use tokio::time;
15+
16+
pub struct TimeoutRequestProgress<S> {
17+
inner: S,
18+
timeout: time::Duration,
19+
}
20+
21+
pub struct TimeoutResponseProgress<S> {
22+
inner: S,
23+
timeout: time::Duration,
24+
}
25+
26+
/// A [`Body`] that imposes a timeout on the amount of time the stream may be
27+
/// stuck waiting for capacity.
28+
#[derive(Debug)]
29+
#[pin_project]
30+
pub struct ProgressTimeoutBody<B> {
31+
#[pin]
32+
inner: B,
33+
sleep: Pin<Box<time::Sleep>>,
34+
timeout: time::Duration,
35+
is_pending: bool,
36+
}
37+
38+
#[derive(Debug, thiserror::Error)]
39+
#[error("body progress timeout after {0:?}")]
40+
pub struct BodyProgressTimeoutError(time::Duration);
41+
42+
// === impl TimeoutRequestProgress ===
43+
44+
impl<S> TimeoutRequestProgress<S> {
45+
pub fn new(timeout: time::Duration, inner: S) -> Self {
46+
Self { inner, timeout }
47+
}
48+
}
49+
50+
impl<B, S> tower_service::Service<http::Request<B>> for TimeoutRequestProgress<S>
51+
where
52+
S: tower_service::Service<http::Request<ProgressTimeoutBody<B>>>,
53+
{
54+
type Response = S::Response;
55+
type Error = S::Error;
56+
type Future = S::Future;
57+
58+
#[inline]
59+
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
60+
self.inner.poll_ready(cx)
61+
}
62+
63+
#[inline]
64+
fn call(&mut self, req: http::Request<B>) -> Self::Future {
65+
self.inner
66+
.call(req.map(|b| ProgressTimeoutBody::new(self.timeout, b)))
67+
}
68+
}
69+
70+
// === impl TimeoutResponseProgress ===
71+
72+
impl<S> TimeoutResponseProgress<S> {
73+
pub fn new(timeout: time::Duration, inner: S) -> Self {
74+
Self { inner, timeout }
75+
}
76+
}
77+
78+
impl<Req, B, S> tower_service::Service<Req> for TimeoutResponseProgress<S>
79+
where
80+
S: tower_service::Service<Req, Response = http::Response<B>>,
81+
S::Future: Send + 'static,
82+
{
83+
type Response = http::Response<ProgressTimeoutBody<B>>;
84+
type Error = S::Error;
85+
type Future =
86+
Pin<Box<dyn std::future::Future<Output = Result<Self::Response, S::Error>> + Send>>;
87+
88+
#[inline]
89+
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
90+
self.inner.poll_ready(cx)
91+
}
92+
93+
#[inline]
94+
fn call(&mut self, req: Req) -> Self::Future {
95+
let timeout = self.timeout;
96+
self.inner
97+
.call(req)
98+
.map_ok(move |res| res.map(|b| ProgressTimeoutBody::new(timeout, b)))
99+
.boxed()
100+
}
101+
}
102+
103+
// === impl ProgressTimeoutBody ===
104+
105+
impl<B> ProgressTimeoutBody<B> {
106+
pub fn new(timeout: time::Duration, inner: B) -> Self {
107+
// Avoid overflows by capping MAX to roughly 30 years.
108+
const MAX: time::Duration = time::Duration::from_secs(86400 * 365 * 30);
109+
Self {
110+
inner,
111+
timeout: timeout.min(MAX),
112+
is_pending: false,
113+
sleep: Box::pin(time::sleep(MAX)),
114+
}
115+
}
116+
}
117+
118+
impl<B> Body for ProgressTimeoutBody<B>
119+
where
120+
B: Body + Send + 'static,
121+
B::Data: Send + 'static,
122+
B::Error: Into<Error>,
123+
{
124+
type Data = B::Data;
125+
type Error = Error;
126+
127+
#[inline]
128+
fn is_end_stream(&self) -> bool {
129+
self.inner.is_end_stream()
130+
}
131+
132+
#[inline]
133+
fn poll_data(
134+
self: Pin<&mut Self>,
135+
cx: &mut Context<'_>,
136+
) -> Poll<Option<Result<Self::Data, Self::Error>>> {
137+
let this = self.project();
138+
*this.is_pending = false;
139+
this.inner.poll_data(cx).map_err(Into::into)
140+
}
141+
142+
#[inline]
143+
fn poll_trailers(
144+
self: Pin<&mut Self>,
145+
cx: &mut Context<'_>,
146+
) -> Poll<Result<Option<HeaderMap<HeaderValue>>, Self::Error>> {
147+
let this = self.project();
148+
*this.is_pending = false;
149+
this.inner.poll_trailers(cx).map_err(Into::into)
150+
}
151+
152+
fn poll_progress(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
153+
let this = self.project();
154+
155+
let _ = this.inner.poll_progress(cx).map_err(Into::into)?;
156+
157+
if !*this.is_pending {
158+
this.sleep
159+
.as_mut()
160+
.reset(time::Instant::now() + *this.timeout);
161+
*this.is_pending = true;
162+
}
163+
164+
match this.sleep.as_mut().poll(cx) {
165+
Poll::Ready(()) => Poll::Ready(Err(BodyProgressTimeoutError(*this.timeout).into())),
166+
Poll::Pending => Poll::Pending,
167+
}
168+
}
169+
170+
#[inline]
171+
fn size_hint(&self) -> http_body::SizeHint {
172+
self.inner.size_hint()
173+
}
174+
}

0 commit comments

Comments
 (0)