buffer's worker closed unexpectedly 与 buffered service failed
主线位置:就绪主线。两个错误都出在
Buffer和它的后台 worker 之间:调用方在poll_ready或者等响应时发现 worker 已经不在了,或者 worker 已经宣布内层坏了。
flowchart TB w["Buffer 的 worker"] --> gone["运行时关闭,worker 被 drop"] w --> bad["内层 poll_ready 返回 Err"] gone --> closed["Closed 错误"] bad --> se["ServiceError 错误"] closed --> forever["之后每个请求都是这个错误"] se --> forever
这两个错误的产生过程(failed 的三步、get_error_on_closed 怎么选错误类型)在 Buffer 里已经逐行讲过。这一页只讲它们在实际项目里怎么出现、怎么修。先记住一点:两种都是永久的,Buffer 没有恢复的办法。
Display | 类型 | 什么时候 |
|---|---|---|
buffer's worker closed unexpectedly | Closed | worker 没经过 failed 就没了 |
buffered service failed: {内层错误} | ServiceError | worker 里内层 poll_ready 返回过 Err |
调用方拿到的都是 BoxError。用 e.is::<tower::buffer::error::Closed>()、e.is::<tower::buffer::error::ServiceError>() 区分;Closed 的 Debug 输出就是 Closed,unwrap() 时看到的是这个词,不是上面那句话。
Closed:测试之间共用一个 Buffer
Buffer::new 用 tokio::spawn(worker) 把 worker 放到当前运行时上。#[tokio::test] 给每个测试建一个自己的运行时,测试结束就关掉。把 Buffer 放进 static,让多个测试共用:
tower = { version = "=0.5.3", features = ["buffer", "util"] }
tokio = { version = "=1.54.1", features = ["full"] }
// tests/shared_buffer.rs
use std::future::Ready;
use std::sync::OnceLock;
use tower::buffer::Buffer;
use tower::{BoxError, Service, ServiceExt, service_fn};
type Client = Buffer<u32, Ready<Result<u32, BoxError>>>;
fn echo(x: u32) -> Ready<Result<u32, BoxError>> {
std::future::ready(Ok(x))
}
// One client for the whole test binary, created by whichever test gets here first.
fn client() -> Client {
static CLIENT: OnceLock<Client> = OnceLock::new();
CLIENT.get_or_init(|| Buffer::new(service_fn(echo), 16)).clone()
}
async fn roundtrip(mut c: Client, x: u32) {
match c.ready().await {
Ok(svc) => assert_eq!(svc.call(x).await.unwrap(), x),
Err(e) => panic!("ready failed: {e} ({e:?})"),
}
}
#[tokio::test]
async fn first() {
roundtrip(client(), 1).await;
}
#[tokio::test]
async fn second() {
roundtrip(client(), 2).await;
}
cargo test --release --test shared_buffer -- --test-threads=1,M2 Max、rustc 1.99.0,连跑 3 次结果相同(线程编号除外):
running 2 tests
. 1/2
second --- FAILED
failures:
---- second stdout ----
thread 'second' (142463) panicked at tests/shared_buffer.rs:21:15:
ready failed: buffer's worker closed unexpectedly (Closed)
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
second
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
first 先跑,Buffer 和 worker 建在 first 的运行时上。first 结束,运行时关闭,上面的任务全部被 drop,worker 连同它的 mpsc::Receiver 一起没了,通道关闭。second 拿到的句柄在 poll_ready 第一行就发现 self.tx.is_closed(),Handle 里没有存过错误,于是是 Closed。
不加 --test-threads=1 时两个测试并行,结果变成看运气。直接跑测试二进制 3 轮、每轮 200 次,失败 182、157、159 次,没有一次卡住。另跑 100 次统计,first 失败 27 次、second 失败 48 次,位置都是 svc.call(x).await.unwrap() 那一行,值都是 Closed:一个测试已经把请求发进了通道,worker 还没回复,就随另一个测试的运行时一起被 drop,oneshot 发送端没发就没了,ResponseFuture 的 Rx 分支返回 Closed。本地并行跑偶尔全绿、CI 上换了线程数就红,是这个坑的常见样子。
修法有两种。最简单的是每个测试自己建 Buffer。确实要共用(比如它背后是一个昂贵的连接),就让 worker 跑在一个和测试二进制同寿的运行时上:用 Buffer::pair 拿到 worker,自己 spawn:
// tests/shared_buffer_fixed.rs:只有 client() 不同
use tokio::runtime::Runtime;
// The worker runs on a runtime that lives as long as the test binary.
fn client() -> Client {
static RT: OnceLock<Runtime> = OnceLock::new();
static CLIENT: OnceLock<Client> = OnceLock::new();
CLIENT
.get_or_init(|| {
let rt = RT.get_or_init(|| {
tokio::runtime::Builder::new_multi_thread()
.worker_threads(1)
.build()
.unwrap()
});
let (client, worker) = Buffer::pair(service_fn(echo), 16);
rt.spawn(worker);
client
})
.clone()
}
--test-threads=1 连跑 3 次都是 test result: ok. 2 passed;不加时直接跑二进制 200 次,200 次通过。调用方在各自测试的运行时里,worker 在 RT 上,Buffer 本来就不要求两者在同一个运行时(Buffer 里「worker 所在的运行时没了」的实验也是跨运行时的)。
Closed 这个错误类型是 2019 年 3 月的 PR #190 加的,标题就是「buffer: fix panics if worker executor drops unexpectedly」。在那之前,响应 future 等 worker 回复时如果发送端被 drop,走的是:
Err(_) => unreachable!(
"Worker exited without sending error to all outstanding requests."
),
unreachable! 表明代码假定 worker 总会先把错误发给每个在等的请求再退出;但执行器直接把 worker 丢掉时,worker 根本没有机会发,这一行就成了 panic。这次提交把这一行换成 Err(_) => return Err(Closed::new().into()),也就是今天 ResponseFuture::poll 的 Rx 分支;同时把 ServiceError 的 Display 从 Service::{method} failed: ... 改成了现在的 buffered service failed: ...。第一个包含它的发布是独立的 tower-buffer 0.1.0,tower 0.1.0 以 tower::buffer 重新导出了它。
buffered service failed:一次错误,永久失败
Service trait 的规则 3:poll_ready 返回 Err 表示这个实例不能再用。Buffer 的 worker 照做:内层 poll_ready 一报错,worker 就把错误存进共享的 Handle、关通道(failed),从此所有句柄都失败。问题在于很多内层服务用 poll_ready 的错误表示「连接断了」,而连接断了本来是可以重连的。
下面的 Conn 是一条连接,1 号连接服务完一个请求后断开,poll_ready 报错:
tower = { version = "=0.5.3", features = ["buffer", "reconnect", "util"] }
tokio = { version = "=1.54.1", features = ["full"] }
use std::future::{Ready, ready};
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
use std::task::{Context, Poll};
use tower::buffer::Buffer;
use tower::reconnect::Reconnect;
use tower::{BoxError, Service, ServiceExt, service_fn};
// A connection. Connection #1 breaks after serving one request:
// its poll_ready reports the broken connection as an error.
struct Conn {
id: u32,
served: u32,
}
impl Service<u32> for Conn {
type Response = String;
type Error = BoxError;
type Future = Ready<Result<String, BoxError>>;
fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), BoxError>> {
if self.id == 1 && self.served >= 1 {
return Poll::Ready(Err("connection reset".into()));
}
Poll::Ready(Ok(()))
}
fn call(&mut self, req: u32) -> Self::Future {
self.served += 1;
ready(Ok(format!("req {req} served by conn #{}", self.id)))
}
}
async fn send<S>(svc: &mut S, req: u32)
where
S: Service<u32, Response = String, Error = BoxError>,
{
match svc.ready().await {
Ok(s) => match s.call(req).await {
Ok(rsp) => println!(" {rsp}"),
Err(e) => println!(" req {req}: call failed: {e}"),
},
Err(e) => println!(" req {req}: ready failed: {e}"),
}
}
#[tokio::main(flavor = "current_thread")]
async fn main() {
println!("Buffer over a connection:");
let mut svc = Buffer::new(Conn { id: 1, served: 0 }, 8);
for req in 1..=3 {
send(&mut svc, req).await;
}
println!("Buffer over Reconnect:");
let next_id = Arc::new(AtomicU32::new(1));
let mk = service_fn(move |_: ()| {
let id = next_id.fetch_add(1, Ordering::SeqCst);
ready(Ok::<_, BoxError>(Conn { id, served: 0 }))
});
let mut svc = Buffer::new(Reconnect::new(mk, ()), 8);
for req in 1..=3 {
send(&mut svc, req).await;
}
}
连跑 3 次输出相同:
Buffer over a connection:
req 1 served by conn #1
req 2: call failed: buffered service failed: connection reset
req 3: ready failed: buffered service failed: connection reset
Buffer over Reconnect:
req 1 served by conn #1
req 2 served by conn #2
req 3 served by conn #2
前半段:
- 请求 2 的
ready成功了,call的结果才失败。Buffer::poll_ready只在通道里预留位置,不问内层;worker 取出请求 2,问内层poll_ready,这时才看到错误,走Poll::Ready(Err(e))分支,把错误回给请求 2。所以只在ready()上处理错误是不够的。 - 请求 3 在
ready上就失败。 通道已经关了,poll_ready第一行返回存着的错误。之后不管内层是不是已经好了,都不会再有人问它。
后半段在 Buffer 和连接之间套了 Reconnect。Reconnect 在已连接状态下遇到内层 poll_ready 报错,不往外传,而是把状态改回 Idle(第 130–133 行),循环回去重新连接。worker 看到的 poll_ready 一直是 Ok,请求 2 由 2 号连接服务。代价是那次断开的错误被丢掉了,连一行日志都没有;Reconnect 只把建连接的错误交给下一次 call(见 Reconnect)。
还有一种写法是在 Buffer 外面检测到 ServiceError 后重建整个 Buffer。这要求所有持有旧句柄的地方都能换成新的,通常比在里面套 Reconnect 麻烦。
小结
Closed(buffer's worker closed unexpectedly):worker 没报错就没了,最常见的原因是 worker 所在的运行时被关掉。测试之间共用一个static Buffer时,串行必失败,并行看运气。修法:每个测试自己建,或用Buffer::pair把 worker 放到一个长寿的运行时上。ServiceError(buffered service failed: …):内层poll_ready报过一次错,Buffer从此永久失败。第一个撞上错误的请求是在响应 future 上失败,之后的在poll_ready上失败。- 内层用
poll_ready的错误表示「连接断了」时,在Buffer里面套Reconnect,让它在内层重连,不把错误传给 worker。