超时一族
主线位置:
Timeout在call时开始计时、在响应 future 完成前把「没有响应」变成一个响应;四个 body 计时器在call时(请求 body)或响应 future 完成时(响应 body)给 body 套一层,真正计时在 body 流动时。TimeoutLayer在 axum 里的行为、为什么超时和内层同时到期时超时赢,见 axum 卷 其余常用中间件。
flowchart TB
subgraph head["到响应头为止"]
t["Timeout:call 时建 Sleep,先 poll Sleep 再 poll 内层,到点回一个空 body 响应"]
end
subgraph reqb["请求 body:call 时套上"]
rbt["RequestBodyTimeout → TimeoutBody"]
rbd["RequestBodyDeadline → DeadlineBody"]
end
subgraph resb["响应 body:future 完成时套上"]
sbt["ResponseBodyTimeout → TimeoutBody"]
sbd["ResponseBodyDeadline → DeadlineBody"]
end
rbt --> err["poll_frame 返回 Err(TimeoutError)"]
rbd --> err
sbt --> err
sbd --> err
五个类型
timeout 模块导出五个中间件(各配一个 Layer)和两个 body:
| 中间件 | 套在哪 | 计时从何时开始 | 何时重置 | 到点后 |
|---|---|---|---|---|
Timeout | 整个响应 future | call | 不重置 | 返回一个状态码自选、空 body 的响应 |
RequestBodyTimeout | 请求 body(TimeoutBody) | handler 第一次 poll 请求 body | 每交出一帧 | 读 body 的一方拿到 TimeoutError |
ResponseBodyTimeout | 响应 body(TimeoutBody) | 调用方第一次 poll 响应 body | 每交出一帧 | 同上 |
RequestBodyDeadline(0.7.0) | 请求 body(DeadlineBody) | call | 不重置 | 同上 |
ResponseBodyDeadline(0.7.0) | 响应 body(DeadlineBody) | 响应 future 完成 | 不重置 | 同上 |
请求方向的两个只是在 call 里 req.map(...),不需要自己的 future,type Future = S::Future(RequestBodyTimeout::call)。响应方向的两个各有一个只做 res.map(...) 的 future(ResponseBodyTimeoutFuture)。四个都不要求 body 实现 Default,因为它们不造响应。
Timeout 要求 ResBody: Default,到点时 Response::new(B::default())(第 141 行)。它的构造函数有两套:TimeoutLayer::new(timeout) 固定回 408,0.6.7 起标了 #[deprecated(since = "0.6.7", note = "Use `TimeoutLayer::with_status_code` instead")](第 29 行,#599);with_status_code(status, timeout) 自选状态码。
TimeoutBody:第一次 poll 才开始,每帧重开
// Start the `Sleep` if not active.
let sleep_pinned = if let Some(some) = this.sleep.as_mut().as_pin_mut() {
some
} else {
this.sleep.set(Some(sleep(*this.timeout)));
this.sleep.as_mut().as_pin_mut().unwrap()
};
// Error if the timeout has expired.
if let Poll::Ready(()) = sleep_pinned.poll(cx) {
return Poll::Ready(Some(Err(Box::new(TimeoutError(())))));
}
// Check for body data.
let frame = ready!(this.body.poll_frame(cx));
// A frame is ready. Reset the `Sleep`...
this.sleep.set(None);
sleep 是 Option<Sleep>,构造时是 None。计时器在 poll_frame 第一次被调用时才建,每拿到一帧就扔掉,下次 poll 再建一个新的。所以它量的是「调用方在等,而数据迟迟不来」的那段时间:两帧之间调用方自己没去 poll 的时间不算。它是一个空闲超时,不是总时长;仓库里有一个专门的文档提交(#574,「Clarify TimeoutBody is a timeout per frame, not total」)说明这一点。
后果就是 0.7.0 加 DeadlineBody 的理由:一个每 50 毫秒发一个字节的客户端,永远不会触发 100 毫秒的 TimeoutBody,一个 10 MiB 的上传能拖上好几天。
DeadlineBody:套上就开始
DeadlineBody(0.7.0,#688)在构造时就建好 Sleep:
pub fn new(timeout: Duration, body: B) -> Self {
DeadlineBody {
sleep: sleep(timeout),
body,
}
}
poll_frame 先查 sleep 再查内层,和 TimeoutBody 一样,只是从不重置。「构造时」对请求 body 是 call 的那一刻,对响应 body 是响应 future 完成的那一刻。计时和有没有人在读无关:响应头交出去以后调用方迟迟不读 body,时间照样在走;到点之后哪怕数据就在缓冲里,第一次 poll 拿到的也是错误。
两者可以叠:一个空闲超时防「连接挂着不动」,一个总时限防「一个字节一个字节地拖」。
错误:TimeoutError
两个 body 的错误类型都是 Box<dyn std::error::Error + Send + Sync>,超时时装的是 TimeoutError,Display 是 data was not received within the designated timeout,可以 downcast 判断。内层 body 自己的错误经 Into<BoxError> 原样透出。
这是一个 body 错误,不是响应:
- 请求方向,错误交给读请求 body 的那一方(handler、提取器),回什么由它决定。
- 响应方向,响应头已经发出去了,没法再换成一个错误响应,服务端只能中断这个响应,客户端读到的是一个不完整的 body。
TimeoutBody 不转发 size_hint
TimeoutBody 的 Body 实现里只有 poll_frame,size_hint 和 is_end_stream 用的是 trait 的默认值:长度未知、永不提前结束。DeadlineBody 两个都转发了。
对响应来说这有实际后果:hyper 在响应没写 Content-Length 头时,靠 body 的 size_hint().exact() 决定发 Content-Length 还是用 chunked 编码。套上 ResponseBodyTimeoutLayer 以后长度信息丢了,原本 5 字节的响应就变成 chunked。下面一个只服务一条连接的 hyper 服务器(只绑 127.0.0.1,发完即关),handler 回 Full::from("hello"),不写任何头:
tower-http = { version = "=0.7.1", features = ["timeout"] }
tower = { version = "=0.5.3", features = ["util"] }
tokio = { version = "=1.54.1", features = ["macros", "rt", "net", "io-util", "time"] }
hyper = { version = "1", features = ["server", "http1"] }
hyper-util = { version = "=0.1.21", features = ["tokio", "service"] }
http = "1"
http-body-util = "0.1"
bytes = "1"
async fn hello(_: Request<hyper::body::Incoming>) -> Result<Response<Full<Bytes>>, Infallible> {
Ok(Response::new(Full::from("hello")))
}
// probe!(label, svc):在 127.0.0.1:0 上 accept 一条连接,用 hyper http1 + TowerToHyperService 服务它;
// 客户端用裸 TcpStream 发 "GET / HTTP/1.1\r\nhost: x\r\nconnection: close\r\n\r\n",
// 读到 EOF,打印 content-length / transfer-encoding 两行
#[tokio::main(flavor = "current_thread")]
async fn main() {
let t = Duration::from_secs(5);
probe!("plain", tower::service_fn(hello));
probe!("ResponseBodyTimeout", ResponseBodyTimeoutLayer::new(t).layer(tower::service_fn(hello)));
probe!("ResponseBodyDeadline", ResponseBodyDeadlineLayer::new(t).layer(tower::service_fn(hello)));
}
输出(hyper 1.12.0,跑 3 次相同):
plain ["content-length: 5"]
ResponseBodyTimeout ["transfer-encoding: chunked"]
ResponseBodyDeadline ["content-length: 5"]
实验:空闲超时与总时限
暂停时钟,毫秒数是 tokio 的虚拟时间。请求 body 用两种节奏:每 50 毫秒一个字节共 10 个(500 毫秒),以及 50、50、150 毫秒三个字节(中间停了 150 毫秒,共 250 毫秒)。响应 body 是现成的 hello,但调用方拿到响应 200 毫秒后才开始读:
tower-http = { version = "=0.7.1", features = ["timeout"] }
tower = { version = "=0.5.3", features = ["util"] }
tokio = { version = "=1.54.1", features = ["macros", "rt", "time", "test-util"] }
http = "1"
http-body = "1"
http-body-util = "0.1"
bytes = "1"
futures = "0.3"
type Body = UnsyncBoxBody<Bytes, BoxError>;
// 每隔 gaps[i] 毫秒发一个字节
fn trickle(gaps: Vec<u64>) -> Body {
let s = futures::stream::unfold(gaps.into_iter(), |mut it| async move {
let gap = it.next()?;
sleep(Duration::from_millis(gap)).await;
Some((Ok::<_, BoxError>(Frame::data(Bytes::from_static(b"x"))), it))
});
StreamBody::new(s).boxed_unsync()
}
// handler 读请求 body,报告结果和读完的时刻;出错时检查 e.is::<TimeoutError>()
async fn read_req<B>(req: Request<B>) -> Result<Response<Full<Bytes>>, Infallible>
where
B: http_body::Body,
B::Error: Into<BoxError>,
{ /* ... */ }
#[tokio::main(flavor = "current_thread", start_paused = true)]
async fn main() {
let even = vec![50; 10];
let gap = vec![50, 50, 150];
println!("request body");
for (name, gaps) in [("even 10x50ms", even.clone()), ("one 150ms gap", gap.clone())] {
let svc = RequestBodyTimeoutLayer::new(Duration::from_millis(100)).layer(tower::service_fn(read_req::<_>));
// oneshot(Request::new(trickle(gaps.clone()))),打印 handler 的回报
let svc = RequestBodyDeadlineLayer::new(Duration::from_millis(300)).layer(tower::service_fn(read_req::<_>));
// 同上
}
println!("response body, read after 200 ms");
let ok = |_: Request<()>| async { Ok::<_, Infallible>(Response::new(Full::<Bytes>::from("hello"))) };
let res = ResponseBodyTimeoutLayer::new(Duration::from_millis(100)).layer(tower::service_fn(ok)).oneshot(Request::new(())).await.unwrap();
println!(" timeout size_hint.exact={:?}", res.body().size_hint().exact());
sleep(Duration::from_millis(200)).await;
println!(" timeout 100ms -> {:?}", res.into_body().collect().await.map(|c| c.to_bytes()));
// ResponseBodyDeadlineLayer 同样走一遍
}
输出(M2 Max、rustc 1.99.0,跑 3 次相同):
request body
timeout 100ms, even 10x50ms -> read 10 bytes at 500 ms
deadline 300ms, even 10x50ms -> error (TimeoutError: true): data was not received within the designated timeout at 300 ms
timeout 100ms, one 150ms gap -> error (TimeoutError: true): data was not received within the designated timeout at 200 ms
deadline 300ms, one 150ms gap -> read 3 bytes at 250 ms
response body, read after 200 ms
timeout size_hint.exact=None
timeout 100ms -> Ok(b"hello")
deadline size_hint.exact=Some(5)
deadline 100ms -> Err("data was not received within the designated timeout")
- 匀速慢传:100 毫秒的空闲超时永远等不到,500 毫秒读完;300 毫秒的总时限在第 300 毫秒切断。这就是 #688 说的「一个字节一个字节慢慢发的客户端只会触发后者」。
- 中间停一下:空闲超时在第三个字节前等了 100 毫秒就报错(50 + 50 + 100 = 200 毫秒);总时限 300 毫秒内读完了 250 毫秒的 body。
- 晚读的响应:
TimeoutBody的计时器从第一次 poll 才开始,晚读 200 毫秒也没事;DeadlineBody从响应 future 完成就开始计时,数据明明就在,第一次 poll 还是拿到超时。size_hint一个丢了、一个保留。
小结
- 五个类型:
Timeout管到响应头为止,到点回一个空 body 响应(new的 408 在 0.6.7 弃用);四个 body 计时器管 body,到点让poll_frame返回TimeoutError。 TimeoutBody是空闲超时:第一次 poll 才建Sleep,每帧之后重建,调用方不 poll 的时间不算。DeadlineBody(0.7.0,#688)是总时限:构造时建Sleep,从不重置,请求方向从call算起,响应方向从响应 future 完成算起,和有没有人读无关。- 超时是 body 错误:请求方向由读 body 的一方处置,响应方向只能中断这个响应。
TimeoutBody不转发size_hint和is_end_stream,套上ResponseBodyTimeoutLayer的响应在 hyper 下从Content-Length变成 chunked;DeadlineBody转发了两者。