Buffer
主线位置:两条主线都在,而且都被切成了两段。就绪主线上,调用方的
poll_ready只问通道里有没有位置,内层的poll_ready由 worker 在另一个任务里问;请求主线上,worker 调用内层拿到 future,经oneshot送回,调用方在自己的任务里把它 poll 完。
sequenceDiagram participant C as 调用方任务 participant Ch as 有界 mpsc participant W as worker 任务 participant S as 内层服务 C->>Ch: poll_ready:预留位置 C->>Ch: call:发出 Message Note over C: call 立即返回 ResponseFuture Ch->>W: 取出消息,位置归还 W->>S: poll_ready S-->>W: Ready(Ok) W->>S: call(request) S-->>W: 内层 future W-->>C: oneshot 送回内层 future Note over C: 在调用方任务里 poll 内层 future
Service::call 要 &mut self,多个任务要用同一个服务,要么每个任务各有一份 Clone,要么排队轮流拿 &mut。很多服务不能 Clone:RateLimit 没有实现它,一条连接也复制不出第二条。Buffer 的做法是让一个后台任务(worker)独占这个服务,对外发出去的只是通道的发送端。发送端可以随便 Clone,所以 Buffer 总是 Clone,不要求内层 Clone。
结构:一个发送端,一个错误槽
#[derive(Debug)]
pub struct Buffer<Req, F> {
tx: PollSender<Message<Req, F>>,
handle: Handle,
}
tx是 tokio-util 的PollSender,包着一个有界mpsc::Sender。handle是Handle,里面只有一个Arc<Mutex<Option<ServiceError>>>,和 worker 共享,worker 失败时把错误放进去。
类型参数是请求类型 Req 和内层 future 的类型 F,没有内层服务的类型 S。0.4 的 Buffer 是对服务泛型的,#654(0.5.0)改成对 future 泛型,PR 的理由是减少类型嵌套:Buffer 外面看不到它包的那一整摞中间件的类型。
new 调 pair 建好通道和 worker,然后 tokio::spawn(worker),所以必须在 tokio 运行时里调用。想用别的执行器就直接用 pair,它把 Worker 还给你自己去 spawn:
pub fn pair<S>(service: S, bound: usize) -> (Self, Worker<S, Req>)
where
S: Service<Req, Future = F> + Send + 'static,
F: Send,
S::Error: Into<crate::BoxError> + Send + Sync,
Req: Send + 'static,
{
let (tx, rx) = mpsc::channel(bound);
let (handle, worker) = Worker::new(service, rx);
let buffer = Self {
tx: PollSender::new(tx),
handle,
};
(buffer, worker)
}
F: Send 不是多余的:内层 future 要从 worker 任务送到调用方任务。BufferLayer 只存一个 bound,layer 就是 Buffer::new;ServiceBuilder::buffer 用的就是它,见 ServiceBuilder。
通道里传的是 Message:
pub(crate) struct Message<Request, Fut> {
pub(crate) request: Request,
pub(crate) tx: Tx<Fut>,
pub(crate) span: tracing::Span,
}
/// Response sender
pub(crate) type Tx<Fut> = oneshot::Sender<Result<Fut, ServiceError>>;
tx 是一个 oneshot 的发送端,送回来的是 Result<Fut, ServiceError>:成功时是内层的 future,不是响应。span 是调用方当时的 tracing span,worker 处理这条消息时会进入它,否则 worker 里产生的日志不会算在这个请求的 span 下。mpsc 和 oneshot 本身怎么实现,见 mpsc 和 oneshot、watch、broadcast、Notify。
poll_ready:预留一个位置
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
// First, check if the worker is still alive.
if self.tx.is_closed() {
// If the inner service has errored, then we error here.
return Poll::Ready(Err(self.get_worker_error()));
}
// Poll the sender to acquire a permit.
self.tx
.poll_reserve(cx)
.map_err(|_| self.get_worker_error())
}
它完全不问内层服务,只问通道。PollSender::poll_reserve(tokio-util 0.7.20)在内部存一个 reserve_owned 的 future,取到 OwnedPermit 后进入 ReadyToSend 状态,许可就存在这个 PollSender 身上,直到 send_item 用掉它,或者 PollSender 被 drop。有界 mpsc 的容量数的是「通道里的消息数加上已经预留、还没发送的许可数」,所以一个 Ready 的 Buffer 句柄实打实地占着一个位置。
PollSender 的 Clone 从 Idle 开始,不带许可。所以 Buffer::clone 出来的副本也不带预留的位置,对一个 ready 的句柄 clone 再去 call 副本会 panic,信息见单服务中间件的表。
is_closed 为真,或者 poll_reserve 发现通道关了,都去 get_worker_error 取错误,后面「内层报错」一节再讲。
#635(0.5.0)之前,Buffer 用的是无界 mpsc 加一个单独的 Semaphore。PR 说明给的原因:升级到当时的 tokio 时,「there was no way to reserve a non-borrowed send permit from a Sender」,poll_ready 拿到的许可存不进结构体里等 call 来用,只好用信号量自己实现一遍有界通道。代价是「a bunch of extra stuff we have to do to e.g. propagate cancellations/service errors to tasks waiting on poll_ready」。这类额外的事出过问题:#480(0.4.0)修的就是 worker 终止或内层失败时,在 poll_ready 上等容量的任务不会被叫醒、永远挂着。
tokio 的 Sender 有了 reserve_owned 以后,#635 换成了一条有界 mpsc,也就是上面的 PollSender::poll_reserve。PR 还提到容量的语义随之变了一点:旧实现里信号量许可跟着消息走,worker 正在处理的那条消息也占着一个位置;换成有界通道后,「capacity is only occupied while a request is actually in the channel」。这一改动要求 Request 和内层 future 满足 Send + 'static,属于破坏性变更,所以等到了 0.5。
call:发出去,马上返回
call:
fn call(&mut self, request: Req) -> Self::Future {
tracing::trace!("sending request to buffer worker");
// get the current Span so that we can explicitly propagate it to the worker
// if we didn't do this, events on the worker related to this span wouldn't be counted
// towards that span since the worker would have no way of entering it.
let span = tracing::Span::current();
// If we've made it here, then a channel permit has already been
// acquired, so we can freely allocate a oneshot.
let (tx, rx) = oneshot::channel();
match self.tx.send_item(Message { request, span, tx }) {
Ok(_) => ResponseFuture::new(rx),
// If the channel is closed, propagate the error from the worker.
Err(_) => {
tracing::trace!("buffer channel closed");
ResponseFuture::failed(self.get_worker_error())
}
}
}
每个请求一次 oneshot 分配。注释说的「a channel permit has already been acquired」就是 poll_ready 预留的那个位置,send_item 用掉它,消息进了通道,call 立即返回。这时内层可能还没就绪,请求也还没交给内层。所以 Timeout 放在 Buffer 外面,排队的时间会算进超时;放在里面就不算,实验见 Timeout。
worker:只调用,不等响应
worker 是一个 Future<Output = ()>,poll 是一个循环:取一条消息,问内层是否就绪,就绪就调用,把 future 送回去,接着取下一条。
loop {
match ready!(self.poll_next_msg(cx)) {
Some((msg, first)) => {
let _guard = msg.span.enter();
if let Some(ref failed) = self.failed {
tracing::trace!("notifying caller about worker failure");
let _ = msg.tx.send(Err(failed.clone()));
continue;
}
// ...
match self.service.poll_ready(cx) {
Poll::Ready(Ok(())) => {
tracing::debug!(service.ready = true, message = "processing request");
let response = self.service.call(msg.request);
// Send the response future back to the sender.
//
// An error means the request had been canceled in-between
// our calls, the response future will just be dropped.
tracing::trace!("returning response future");
let _ = msg.tx.send(Ok(response));
}
Poll::Pending => {
tracing::trace!(service.ready = false, message = "delay");
// Put out current message back in its slot.
drop(_guard);
self.current_message = Some(msg);
return Poll::Pending;
}
Poll::Ready(Err(e)) => {
// ...
self.failed(error);
// ...
}
}
}
None => {
// No more more requests _ever_.
self.finish = true;
return Poll::Ready(());
}
}
}
三点:
- 内层没就绪时,消息留在 worker 手里(
current_message),不放回通道。消息被poll_recv取出的那一刻,它在通道里的位置就还回去了,所以内层卡住时,worker 手里这一条不占bound。 - worker 不等响应。
call一返回就把 future 送走,接着处理下一条。内层能同时有多少个请求在途,Buffer不管,要限制就在里面套 ConcurrencyLimit:这时 worker 的poll_ready要等某个调用方把它的响应 future poll 完、许可归还,才会继续。 - 请求是一条一条按顺序交给内层的。worker 只有一个,内层也只有一个,先进通道的先被调用。
实验:谁在 poll 什么
内层服务在 poll_ready、call 和 future 体里分别打印当前任务的 ID:
tower = { version = "=0.5.3", features = ["buffer", "util"] }
tokio = { version = "=1.54.1", features = ["full"] }
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use tower::buffer::Buffer;
use tower::{Service, ServiceExt};
// An inner service that is not Clone. It reports which task runs each step.
struct Echo;
impl Service<u32> for Echo {
type Response = u32;
type Error = std::convert::Infallible;
type Future = Pin<Box<dyn Future<Output = Result<u32, Self::Error>> + Send>>;
fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
println!("inner poll_ready in task {:?}", tokio::task::try_id());
Poll::Ready(Ok(()))
}
fn call(&mut self, req: u32) -> Self::Future {
println!("inner call in task {:?}", tokio::task::try_id());
Box::pin(async move {
println!("inner future body in task {:?}", tokio::task::try_id());
Ok(req * 2)
})
}
}
#[tokio::main(flavor = "current_thread")]
async fn main() {
let svc = Buffer::new(Echo, 8);
let caller = tokio::spawn(async move {
println!("caller is task {:?}", tokio::task::try_id());
let mut svc = svc;
let rsp = svc.ready().await.unwrap().call(21).await.unwrap();
println!("response = {rsp}");
});
caller.await.unwrap();
}
M2 Max、rustc 1.99.0、--release,连跑 3 次输出相同:
caller is task Some(Id(2))
inner poll_ready in task Some(Id(1))
inner call in task Some(Id(1))
inner future body in task Some(Id(2))
response = 42
任务 1 是 Buffer::new spawn 的 worker,任务 2 是调用方。内层的 poll_ready 和 call 在 worker 里跑,call 返回的 future 体在调用方的任务里跑。一个请求慢,只拖慢 poll 它的那个调用方,worker 照样能把下一个请求交给内层。
配菜间只有一个配菜师(worker),只有他能用配菜台(内层服务)。出单夹的数量有限(bound)。服务员先抢一个空夹子(poll_ready 预留位置),抢到才去点单;点好的单子夹上去,附一个取餐呼叫器(call 发 Message,里面有 oneshot)。配菜师从夹子上取下单子,夹子就空出来了(poll_recv 归还位置)。配菜台没备好时,他手里拿着这张单等(current_message)。备好了就配一份锅底和菜,交还给那一桌(送回内层 future),客人在自己桌上煮(调用方任务 poll future)。配菜师不看谁煮得慢,转身配下一单。客人等不及走了(ResponseFuture 被 drop,呼叫器没人接),轮到这张单时他直接扔掉。
比方到这里为止:真的配菜师交出去就跟这桌没关系了;内层 future 却可能还和内层服务共享资源。内层是 ConcurrencyLimit 时,许可跟着 future 到了调用方那里,客人不吃完,配菜台就一直显示「没备好」。
响应在调用方任务里完成
ResponseFuture 有三个状态(ResponseState):Failed 是 call 时就知道失败了,Rx 在等 oneshot,Poll 拿到了内层 future。poll:
loop {
match this.state.as_mut().project() {
ResponseStateProj::Failed { error } => {
return Poll::Ready(Err(error.take().expect("polled after error")));
}
ResponseStateProj::Rx { rx } => match ready!(rx.poll(cx)) {
Ok(Ok(fut)) => this.state.set(ResponseState::Poll { fut }),
Ok(Err(e)) => return Poll::Ready(Err(e.into())),
Err(_) => return Poll::Ready(Err(Closed::new().into())),
},
ResponseStateProj::Poll { fut } => return fut.poll(cx).map_err(Into::into),
}
}
oneshot 送来 future 之后,状态换成 Poll,从此和 worker 再无往来。oneshot 的发送端没发就被 drop(worker 没了),返回 Closed。
为什么送 future 而不是送响应:如果 worker 自己把 future poll 到完成再送响应,它就得同时管着所有在途请求的 future,慢请求会占住它;只送 future,worker 每个请求只做一次 poll_ready 加一次 call。代价是每个请求一个 oneshot,以及内层 future 必须 Send。这个取舍在第 08 部分还会和其他设计放在一起比较。
问: 内层某个请求返回了 Err,这个 Buffer 会不会从此报废?
答: 不会。请求的错误是内层 future 解析出来的,而内层 future 在调用方的任务里 poll,错误经 ResponseFuture 的 map_err(Into::into) 只交给这一个调用方,worker 根本看不到。只有 worker 里的内层 poll_ready 返回 Err,也就是内层说「这个服务坏了」,才会让整个 Buffer 失败。这和 Service trait 里两种错误的区分是同一件事。
内层报错:一个错误,所有人共享
内层 poll_ready 返回 Err 时,worker 调 failed:
fn failed(&mut self, error: crate::BoxError) {
// ...
// We do this by *first* exposing the error, *then* closing the channel used to
// send more requests (so the client will see the error when the send fails), and *then*
// sending the error to all outstanding requests.
let error = ServiceError::new(error);
let mut inner = self.handle.inner.lock().unwrap();
if inner.is_some() {
// Future::poll was called after we've already errored out!
return;
}
*inner = Some(error.clone());
drop(inner);
self.rx.close();
// By closing the mpsc::Receiver, we know that poll_next_msg will soon return Ready(None),
// which will trigger the `self.finish == true` phase. We just need to make sure that any
// requests that we receive before we've exhausted the receiver receive the error:
self.failed = Some(error);
}
顺序是注释里强调的三步:先把错误放进共享的 Handle,再关通道,最后把错误发给已经在通道里的请求。要避免的情况是:错误已经发给了所有在排队的请求,这之后又有一个调用方成功发进来一条,结果谁也不回它。先公开错误再关通道,保证每个调用方要么发送失败、从 Handle 读到错误,要么它的消息在通道里、会被上面循环里的 if let Some(ref failed) 分支回一个错误。
关通道还有一个作用:有界 mpsc 的接收端 close 会关掉通道的信号量,正在 poll_reserve 上等位置的调用方会被叫醒,拿到错误。这正是 #480 当年要手工补的那件事。
错误类型有两个(error.rs):
| 类型 | Display | 什么时候 |
|---|---|---|
ServiceError | buffered service failed: {内层错误} | worker 里内层 poll_ready 报过错 |
Closed | buffer's worker closed unexpectedly | worker 没报错就没了,比如它所在的运行时被关掉 |
ServiceError 里是 Arc<BoxError>,所以同一个内层错误可以分给任意多个调用方,clone 只是加一次引用计数(这个 clone 是 pub(crate) 的,注释说不想把 Clone 暴露成公开 API)。source() 返回内层的原始错误。调用方拿到的是 BoxError,要 downcast_ref::<ServiceError>() 才能认出它。
get_error_on_closed 决定 Buffer 这一侧报哪个:Handle 里有错误就是 ServiceError,没有就是 Closed。
实验:三个调用方,一个错误
bound 为 1。内层一开始 Pending,之后 poll_ready 返回错误。请求 1 在 worker 手里,请求 2 占着通道里唯一的位置,调用方 c 在 poll_ready 上等位置:
tower = { version = "=0.5.3", features = ["buffer", "util"] }
tokio = { version = "=1.54.1", features = ["full"] }
use std::future::{ready, Ready};
use std::sync::{Arc, Mutex};
use std::task::{Context, Poll, Waker};
use tower::buffer::{error::ServiceError, Buffer};
use tower::{BoxError, Service, ServiceExt};
// Pending until `fail` is set, then poll_ready returns an error.
#[derive(Default)]
struct Switch {
fail: bool,
waker: Option<Waker>,
}
struct Flaky(Arc<Mutex<Switch>>);
impl Service<u32> for Flaky {
type Response = u32;
type Error = BoxError;
type Future = Ready<Result<u32, BoxError>>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), BoxError>> {
let mut s = self.0.lock().unwrap();
if s.fail {
return Poll::Ready(Err("backend down".into()));
}
s.waker = Some(cx.waker().clone());
Poll::Pending
}
fn call(&mut self, req: u32) -> Self::Future {
ready(Ok(req))
}
}
#[tokio::main(flavor = "current_thread")]
async fn main() {
let switch = Arc::new(Mutex::new(Switch::default()));
let svc = Buffer::new(Flaky(switch.clone()), 1);
// Fill the worker's hand and the single channel slot.
let mut a = svc.clone();
let fa = a.ready().await.unwrap().call(1);
tokio::task::yield_now().await;
let mut b = svc.clone();
let fb = b.ready().await.unwrap().call(2);
// c waits in poll_ready for a slot.
let mut c = svc.clone();
let waiter = tokio::spawn(async move { c.ready().await.map(|_| ()) });
tokio::task::yield_now().await;
println!("c finished before failure: {}", waiter.is_finished());
let w = {
let mut s = switch.lock().unwrap();
s.fail = true;
s.waker.take()
};
w.unwrap().wake();
println!("a: {}", fa.await.unwrap_err());
println!("b: {}", fb.await.unwrap_err());
let e = waiter.await.unwrap().unwrap_err();
println!("c: {e}");
// The boxed error is a ServiceError; its source is the inner error.
let source = std::error::Error::source(&*e).map(|s| s.to_string());
println!("c is ServiceError: {}, source: {source:?}", e.is::<ServiceError>());
}
连跑 3 次输出相同:
c finished before failure: false
a: buffered service failed: backend down
b: buffered service failed: backend down
c: buffered service failed: backend down
c is ServiceError: true, source: Some("backend down")
三个调用方从三条不同的路拿到同一个错误:a 的消息在 worker 手里,Poll::Ready(Err(e)) 分支直接回它;b 的消息还在通道里,worker 关通道后继续取消息,走 if let Some(ref failed) 分支;c 根本没发出消息,它在 poll_reserve 上被关通道叫醒,经 get_worker_error 从 Handle 读到错误。之后再来的调用方,poll_ready 第一行的 is_closed() 就为真,也是这个错误。Buffer 没有恢复的办法,要换一个新的。内层是一条可能断开的连接时,通常在 Buffer 里面套 Reconnect,它不会因为连不上就让 poll_ready 报错。
实验:worker 所在的运行时没了
use tower::buffer::Buffer;
use tower::{service_fn, BoxError, Service, ServiceExt};
fn main() {
// The worker is spawned on rt1, a runtime with its own thread.
let rt1 = tokio::runtime::Builder::new_multi_thread()
.worker_threads(1)
.build()
.unwrap();
let mut svc = rt1.block_on(async {
Buffer::new(service_fn(|x: u32| async move { Ok::<_, BoxError>(x) }), 4)
});
// The caller lives on rt2.
let rt2 = tokio::runtime::Builder::new_current_thread().build().unwrap();
rt2.block_on(async {
let r = svc.ready().await.unwrap().call(1).await;
println!("rt1 alive : {:?}", r.map_err(|e| e.to_string()));
});
// Shutting rt1 down drops the worker task.
drop(rt1);
rt2.block_on(async {
match svc.ready().await {
Ok(_) => println!("rt1 dropped: ready"),
Err(e) => println!("rt1 dropped: {e}"),
}
});
}
依赖同上,连跑 3 次输出相同:
rt1 alive : Ok(1)
rt1 dropped: buffer's worker closed unexpectedly
运行时关闭时会 drop 它上面的所有任务,worker 连同它的 mpsc::Receiver 一起没了,通道关闭。worker 没有经过 failed,Handle 里是 None,所以报 Closed。常见的场景是把 Buffer 存进一个 static 或全局对象,然后在多个 #[tokio::test] 之间共用:每个测试各有一个运行时,建 Buffer 的那个测试结束,worker 就跟着没了。
调用方放弃的请求
调用方的 ResponseFuture 被 drop,oneshot 的接收端也就没了。worker 在 poll_next_msg 里检查这一点,手里那条和新取出来的都查:
if let Some(msg) = self.current_message.take() {
// If the oneshot sender is closed, then the receiver is dropped,
// and nobody cares about the response. If this is the case, we
// should continue to the next request.
if !msg.tx.is_closed() {
tracing::trace!("resuming buffered request");
return Poll::Ready(Some((msg, false)));
}
tracing::trace!("dropping cancelled buffered request");
}
// Get the next request
while let Some(msg) = ready!(Pin::new(&mut self.rx).poll_recv(cx)) {
if !msg.tx.is_closed() {
tracing::trace!("processing new request");
return Poll::Ready(Some((msg, true)));
}
// Otherwise, request is canceled, so pop the next one.
tracing::trace!("dropping cancelled request");
}
内层就绪之前就被放弃的请求,内层根本看不到。如果放弃发生在 worker 已经 call 了内层之后,msg.tx.send(Ok(response)) 会失败,内层 future 被丢掉,一次都没被 poll;但内层 call 里同步做的事已经做了。
下面的程序里内层先不就绪,三个请求排上队后,前两个调用方放弃:
use std::future::{ready, Ready};
use std::sync::{Arc, Mutex};
use std::task::{Context, Poll, Waker};
use tower::buffer::Buffer;
use tower::{BoxError, Service, ServiceExt};
// Not ready until someone opens the gate.
#[derive(Default)]
struct Gate {
open: bool,
waker: Option<Waker>,
}
struct Gated(Arc<Mutex<Gate>>);
impl Service<u32> for Gated {
type Response = u32;
type Error = BoxError;
type Future = Ready<Result<u32, BoxError>>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), BoxError>> {
let mut g = self.0.lock().unwrap();
if g.open {
return Poll::Ready(Ok(()));
}
g.waker = Some(cx.waker().clone());
Poll::Pending
}
fn call(&mut self, req: u32) -> Self::Future {
println!("inner call({req})");
ready(Ok(req))
}
}
#[tokio::main(flavor = "current_thread")]
async fn main() {
let gate = Arc::new(Mutex::new(Gate::default()));
let svc = Buffer::new(Gated(gate.clone()), 8);
let mut futs = Vec::new();
for i in 1..=3 {
let mut h = svc.clone();
futs.push(h.ready().await.unwrap().call(i));
tokio::task::yield_now().await; // let the worker pick it up
}
// Callers of 1 and 2 give up before the inner service is ready.
let f3 = futs.pop().unwrap();
drop(futs);
let waker = {
let mut g = gate.lock().unwrap();
g.open = true;
g.waker.take()
};
waker.unwrap().wake();
println!("f3 = {}", f3.await.unwrap());
}
依赖同上,连跑 3 次输出相同:
inner call(3)
f3 = 3
请求 1 在 worker 手里(current_message),请求 2 在通道里,两条都被跳过,内层只被 call 了一次。
bound 怎么选
Buffer::new 的文档专门有一节「A note on choosing a bound」:
/// When [`Buffer`]'s implementation of [`poll_ready`] returns [`Poll::Ready`], it reserves a
/// slot in the channel for the forthcoming [`call`]. However, if this call doesn't arrive,
/// this reserved slot may be held up for a long time. As a result, it's advisable to set
/// `bound` to be at least the maximum number of concurrent requests the [`Buffer`] will see.
/// If you do not, all the slots in the buffer may be held up by futures that have just called
/// [`poll_ready`] but will not issue a [`call`], which prevents other senders from issuing new
/// requests.
预留的位置记在句柄的 PollSender 里,句柄不 call、也不 drop,位置就一直占着,哪怕内层完全空闲。Service trait 的第 4 条规则说 call 可能永远不来,Buffer 遵守了它(drop 句柄会归还),但「不来、也不走」的句柄它管不了。
use std::time::Duration;
use tokio::time::timeout;
use tower::buffer::Buffer;
use tower::{service_fn, BoxError, Service, ServiceExt};
#[tokio::main(flavor = "current_thread", start_paused = true)]
async fn main() {
let svc = Buffer::new(service_fn(|x: u32| async move { Ok::<_, BoxError>(x) }), 2);
// Two handles reserve a slot each, then go off and do something else.
let mut h1 = svc.clone();
let mut h2 = svc.clone();
h1.ready().await.unwrap();
h2.ready().await.unwrap();
println!("h1, h2 reserved, not calling");
// A third caller cannot even get a slot, though the inner service is idle.
let mut c = svc.clone();
let r = timeout(Duration::from_secs(5), c.ready()).await;
println!("c.ready within 5s: {}", if r.is_ok() { "ok" } else { "timed out" });
// Dropping a handle that holds a reservation gives the slot back.
drop(h1);
let r = timeout(Duration::from_secs(5), c.ready()).await;
println!("after drop(h1): {}", if r.is_ok() { "ok" } else { "timed out" });
println!("c.call(7) = {}", c.call(7).await.unwrap());
}
start_paused 需要 tokio 的 test-util feature(tokio = { version = "=1.54.1", features = ["full", "test-util"] })。连跑 3 次输出相同:
h1, h2 reserved, not calling
c.ready within 5s: timed out
after drop(h1): ok
c.call(7) = 7
内层是一个永远就绪的 service_fn,c 照样等不到位置。真实程序里,「ready 了但还不 call」的句柄通常是在 ready().await 之后又等了别的东西:一把锁、另一个服务的就绪、或者另一个经过同一个 Buffer 的请求的结果。最后一种会直接死锁。文档建议 bound 不小于「最大并发请求数」,这里的数要按句柄算:一个任务先 ready 了一个句柄,再经另一个句柄发请求,它一个人就要占两个位置。
bound 也决定了背压从哪里开始:通道满了,所有调用方的 poll_ready 都 Pending。.buffer(100).concurrency_limit(10) 这样的组合里,最多 100 个排在通道里,10 个交给了内层,见 ServiceBuilder。
内层永远不就绪。每个新句柄给 10ms 去 ready(),就绪了就 call 并把 future 留着:
// Never ready (in this demo nobody ever needs to wake it).
struct Stuck;
impl Service<u32> for Stuck {
type Response = u32;
type Error = BoxError;
type Future = Ready<Result<u32, BoxError>>;
fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), BoxError>> {
Poll::Pending
}
fn call(&mut self, req: u32) -> Self::Future {
ready(Ok(req))
}
}
#[tokio::main(flavor = "current_thread", start_paused = true)]
async fn main() {
let svc = Buffer::new(Stuck, 1);
let mut in_flight = Vec::new();
for i in 0..5 {
let mut h = svc.clone();
match timeout(Duration::from_millis(10), h.ready()).await {
Ok(Ok(_)) => in_flight.push(h.call(i)),
_ => {
println!("accepted {} requests, request {i} blocks", in_flight.len());
break;
}
}
}
}
动脑筋答案
M2 Max、rustc 1.99.0、--release,连跑 3 次输出相同:
accepted 2 requests, request 2 blocks
- 请求 0:
poll_reserve拿到唯一的位置,call发进通道。等第二个句柄ready()的那 10ms 里,worker 运行,poll_recv取走请求 0,位置归还;内层Pending,请求 0 留在current_message。 - 请求 1:位置又空了,预留、发送成功,它停在通道里。worker 卡在内层的
poll_ready上,不会再取。 - 请求 2:通道里一条消息占满了
bound,poll_reserve一直Pending,10ms 后超时。
所以内层卡住时,Buffer 能收下的请求是 bound + 1:多出来的那一个在 worker 手里,不占通道。#635 的说明里那个「细微差别」就是这个:0.4 的实现里消息带着信号量许可,worker 手里那条也占一个位置,同样的程序只会收下 1 个。把依赖换成 tower = "=0.4.13"(buffer、util 两个 feature)原样跑,连跑 3 次都是:
accepted 1 requests, request 1 blocks
小结
Buffer让一个后台 worker 独占内层服务,对外只发PollSender;它总能Clone,不要求内层Clone。new要在 tokio 运行时里调,pair把 worker 交给你自己 spawn。poll_ready不问内层,只用poll_reserve在有界mpsc里预留一个位置;call把请求、oneshot发送端和当前 span 发过去,立即返回。所以Timeout在Buffer外面会把排队时间算进去。- worker 逐条取消息,内层就绪就
call,把内层 future 经oneshot送回,不等响应;响应在调用方的任务里完成,所以内层 future 要Send。 - 只有内层
poll_ready的错误让 worker 失败:错误包成Arc放进共享槽,先公开再关通道再逐条回复,之后所有调用方都拿到同一个buffered service failed: …。worker 没报错就没了,是buffer's worker closed unexpectedly。 - 调用方放弃的请求,worker 在调用内层之前跳过。
- 预留的位置只有
call或 drop 句柄才会归还;bound至少要等于同时使用它的任务数,否则 ready 了不 call 的句柄会把位置占光。内层卡住时实际能收下bound + 1个请求。