Rust in Everything
基于 tower 0.5.3

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。

结构:一个发送端,一个错误槽

Buffer:

#[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:预留一个位置

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 取错误,后面「内层报错」一节再讲。

📝NOTE
冷知识:0.4 的 Buffer 自己拼了一个有界通道

#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 照样能把下一个请求交给内层。

💡TIP
打个比方:火锅店的配菜间

配菜间只有一个配菜师(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 部分还会和其他设计放在一起比较。

📝NOTE
没有蠢问题

问: 内层某个请求返回了 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什么时候
ServiceErrorbuffered service failed: {内层错误}worker 里内层 poll_ready 报过错
Closedbuffer's worker closed unexpectedlyworker 没报错就没了,比如它所在的运行时被关掉

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。

❗IMPORTANT
动脑筋:bound 为 1,能收下几个请求?

内层永远不就绪。每个新句柄给 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;
      }
    }
  }
}
打印出来的 accepted 是几?答案在本页末尾。

动脑筋答案

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 个请求。

💬 关联讨论

发起讨论
加载中...