Rust in Everything
基于 tower 0.5.3

Service trait

主线位置:两条主线的起点。poll_ready 是就绪主线上的每一级台阶,call 返回的 Future 是请求主线上被一层层包起来的那个东西。

flowchart TB
  start[调用方] --> pr["poll_ready(cx)"]
  pr -->|Pending| wait["登记 waker,等唤醒"]
  wait --> pr
  pr -->|"Ready(Ok)"| ok[已就绪]
  ok -->|再问一次| pr2["仍须 Ready"]
  ok --> callreq["call(req)"]
  callreq --> fut[返回 Future]
  ok -->|不 call 就 drop| rel[释放预留的资源]
  pr -->|"Ready(Err)"| dead[实例报废,丢弃]

整个 tower 生态都建在 tower-service 这个 crate 上,而它只有一个文件 tower-service/src/lib.rs,401 行,去掉注释和空行只剩 52 行,其余都是文档。文档不是装饰:poll_ready 的契约全写在注释里,编译器检查不了,后面每个中间件都按这几段注释来实现。

💡TIP
打个比方:餐厅订位

poll_ready 是打电话订位:没空桌时店家记下你的电话,有空了回电(Pending + waker);订到之后你再打一次,桌子还是你的(重复 poll_ready 仍是 Ready);店家倒闭了会直接告诉你别再打了(Ready(Err))。call 是人到店里点菜。你订了位却没来,店家得能把桌子放出去(drop 时释放预留的资源)。你的朋友照着你的订单号去吃饭是不行的,他没订位(clone 出来的副本不带就绪状态)。

比方到这里为止:订位只有一张桌子,而 tower 里每一层中间件都可能各自「订」一样东西(信号量许可、通道位置、本周期的剩余次数),一次 poll_ready 是一串订位电话。

定义:两个方法,三个关联类型

Service 去掉文档注释后是这样:

pub trait Service<Request> {
    type Response;
    type Error;
    type Future: Future<Output = Result<Self::Response, Self::Error>>;

    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>>;

    #[must_use = "futures do nothing unless you `.await` or poll them"]
    fn call(&mut self, req: Request) -> Self::Future;
}

几个形状上的选择:

位置选择结果
Request泛型参数,不是关联类型同一个类型可以对多种请求类型各实现一次 Service
Response、Error、Future关联类型请求类型定了,这三样就唯一确定,中间件可以写 S::Future 包进自己的 future
两个方法的接收者&mut self,不是 Pin<&mut Self>服务本身不需要 pin,可以随便移动;ServiceExt 的 future 正是靠这一点手写 Unpin,见 ServiceExt 与 Oneshot
poll_ready带 cx能登记 waker,所以能返回 Pending
call不带 cx,同步返回一个 future不能等待;要等的东西放进返回的 future 里

Future 是关联类型而不是 Box<dyn Future>,所以 Timeout<ConcurrencyLimit<S>> 的 future 类型是 timeout::ResponseFuture<concurrency::ResponseFuture<S::Future>>,一层套一层,这几层包装本身不做堆分配。这就是请求主线「future 往外包」在类型上的样子。

tag 里的 tower-service 是 #![no_std],只 extern crate alloc 拿一个 Box,用在后面的转发实现上。

poll_ready 的四条规则

规则全在 poll_ready 的文档注释里(第 332–350 行),原文:

    /// Returns `Poll::Ready(Ok(()))` when the service is able to process requests.
    ///
    /// If the service is at capacity, then `Poll::Pending` is returned and the task
    /// is notified when the service becomes ready again. This function is
    /// expected to be called while on a task. Generally, this can be done with
    /// a simple [`std::future::poll_fn`] call.
    ///
    /// If `Poll::Ready(Err(_))` is returned, the service is no longer able to service requests
    /// and the caller should discard the service instance.
    ///
    /// Once `poll_ready` returns `Poll::Ready(Ok(()))`, a request may be dispatched to the
    /// service using `call`. Until a request is dispatched, repeated calls to
    /// `poll_ready` must return either `Poll::Ready(Ok(()))` or `Poll::Ready(Err(_))`.
    ///
    /// Note that `poll_ready` may reserve shared resources that are consumed in a subsequent
    /// invocation of `call`. Thus, it is critical for implementations to not assume that `call`
    /// will always be invoked and to ensure that such resources are released if the service is
    /// dropped before `call` is invoked or the future returned by `call` is dropped before it
    /// is polled.
    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>>;

拆成四条:

#规则出处约束谁
1返回 Pending 时,服务负责在变成就绪时唤醒当前任务;所以 poll_ready 要在任务里调用第 334–337 行实现方:不能只返回 Pending 不登记 waker
2返回过 Ready(Ok) 之后、call 之前,再问必须还是 Ready(Ok) 或 Ready(Err),不能退回 Pending第 342–344 行实现方:就绪是一种「已占到」的状态,重复问不能再占一份,也不能弄丢
3返回 Ready(Err) 表示这个实例不能再用,调用方应当丢掉它第 339–340 行调用方:不要对报过错的实例继续 poll_ready / call
4poll_ready 可以预留共享资源,但 call 可能永远不来;服务在 call 之前被 drop、或 call 返回的 future 没被 poll 就被 drop,资源都要释放第 346–350 行实现方:资源要放在 drop 时会归还的东西里

call 那一侧还有两条,在 call 的注释里(第 353–364 行):

    /// Process the request and return the response asynchronously.
    ///
    /// This function is expected to be callable off task. As such,
    /// implementations should take care to not call `poll_ready`.
    ///
    /// Before dispatching a request, `poll_ready` must be called and return
    /// `Poll::Ready(Ok(()))`.
    ///
    /// # Panics
    ///
    /// Implementations are permitted to panic if `call` is invoked without
    /// obtaining `Poll::Ready(Ok(()))` from `poll_ready`.
  • call 可能不在任务里被调用,所以它不能调 poll_ready(手里也没有 cx)。
  • 调用方必须先拿到 Ready(Ok) 再 call;没拿到就 call,实现方可以 panic。

规则 2 和规则 4 合起来,就是就绪主线的做法:每一层在 poll_ready 里「先占资源再问内层」,占到的资源存在 self 里,重复问时直接复用;call 把资源移进返回的 future,future 结束或被 drop 时归还。

在 ConcurrencyLimit 上看这几条规则

ConcurrencyLimit 是最直接的例子(细节见 ConcurrencyLimit)。它的 poll_ready 和 call:

    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
        // If we haven't already acquired a permit from the semaphore, try to
        // acquire one first.
        if self.permit.is_none() {
            self.permit = ready!(self.semaphore.poll_acquire(cx));
            // ...
        }

        // Once we've acquired a permit (or if we already had one), poll the
        // inner service.
        self.inner.poll_ready(cx)
    }

    fn call(&mut self, request: Request) -> Self::Future {
        // Take the permit
        let permit = self
            .permit
            .take()
            .expect("max requests in-flight; poll_ready must be called first");

        // Call the inner service
        let future = self.inner.call(request);

        ResponseFuture::new(future, permit)
    }
  • 规则 1:许可不够时,poll_acquire 把 waker 登记在 tokio 信号量的等待队列里,ready! 把 Pending 往上传。
  • 规则 2:self.permit.is_none() 这个判断保证重复 poll_ready 不会再取一个许可。
  • 规则 4:许可是 OwnedSemaphorePermit,它的 Drop 会归还。服务被 drop,self.permit 跟着 drop;call 之后许可在 ResponseFuture 里,future 被 drop 也一样。
  • call 的 panic:self.permit 是 None 就 expect。

而它的 Clone 实现故意不复制许可:

impl<T: Clone> Clone for ConcurrencyLimit<T> {
    fn clone(&self) -> Self {
        // Since we hold an `OwnedSemaphorePermit`, we can't derive `Clone`.
        // Instead, when cloning the service, create a new service with the
        // same semaphore, but with the permit in the un-acquired state.
        Self {
            inner: self.inner.clone(),
            semaphore: self.semaphore.clone(),
            permit: None,
        }
    }
}

下面的程序在一个只有 1 个许可的信号量上验证这些行为。tokio_test::task::spawn(()) 给出一个假任务,enter 用它的 Context 调 poll_ready,is_woken 查这个任务有没有被唤醒。

tower = { version = "=0.5.3", features = ["limit", "util"] }
tokio = { version = "=1.54.1", features = ["full"] }
tokio-test = "0.4"
use std::convert::Infallible;
use std::panic;
use std::sync::Arc;
use tokio::sync::Semaphore;
use tokio_test::task;
use tower::limit::ConcurrencyLimit;
use tower::{service_fn, Service};

fn main() {
  let sem = Arc::new(Semaphore::new(1));
  let echo = service_fn(|req: u32| async move { Ok::<_, Infallible>(req) });
  let mut a = ConcurrencyLimit::with_semaphore(echo, sem.clone());

  let mut ta = task::spawn(());
  let r1 = ta.enter(|cx, _| a.poll_ready(cx));
  let r2 = ta.enter(|cx, _| a.poll_ready(cx));
  println!("a: {:?} then {:?}, permits left = {}", r1, r2, sem.available_permits());

  let mut b = a.clone();
  let mut tb = task::spawn(());
  let rb = tb.enter(|cx, _| b.poll_ready(cx));
  println!("b = a.clone(): {:?}", rb);

  let caught = panic::catch_unwind(panic::AssertUnwindSafe(|| {
    let _ = b.call(1);
  }));
  let msg = caught.unwrap_err();
  println!("b.call panicked: {:?}", msg.downcast_ref::<String>().unwrap());

  drop(a);
  println!("drop(a) without call: permits left = {}, b woken = {}", sem.available_permits(), tb.is_woken());
  let rb = tb.enter(|cx, _| b.poll_ready(cx));
  println!("b again: {:?}, permits left = {}", rb, sem.available_permits());
}

M2 Max、rustc 1.99.0、--release,连跑 3 次输出相同(panic 那几行是默认 panic hook 打到 stderr 的,线程编号每次不同):

a: Ready(Ok(())) then Ready(Ok(())), permits left = 0
b = a.clone(): Pending

thread 'main' (16795896) panicked at ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tower-0.5.3/src/limit/concurrency/service.rs:86:14:
max requests in-flight; poll_ready must be called first
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
b.call panicked: "max requests in-flight; poll_ready must be called first"
drop(a) without call: permits left = 0, b woken = true
b again: Ready(Ok(())), permits left = 0

逐行看:

  • 第 1 行:a 连问两次都是 Ready,信号量里只少了 1 个许可。这是规则 2。
  • 第 2 行:b 是 a 的克隆,共享同一个信号量,但 permit: None。唯一的许可在 a 手里,所以 b 是 Pending,它的 waker 进了信号量的等待队列。
  • panic:对没就绪的 b 调 call,在 service.rs 第 86 行的 expect 处 panic,信息是 max requests in-flight; poll_ready must be called first。这是「允许 panic」的那一条。
  • drop(a):a 从没 call,被 drop 时许可归还(规则 4),而且 b 的任务被唤醒了(规则 1 从 b 这一侧看)。permits left 仍是 0:tokio 的信号量释放许可时先分给排队的等待者,许可直接记到了 b 的等待节点上,没有回到计数里,见 batch_semaphore。
  • 最后一行:b 再问一次就拿到了那个许可。
📝NOTE
冷知识:最早的契约是「call 前不必 poll_ready」

2019 年 2 月以前,tower-service 的文档写的是另一套规则:poll_ready 是 best effort,允许误报,「It is permitted for the service to return Ready from a poll_ready call and the next invocation of call results in an error」;call 那边则写着「Calling call without calling poll_ready is permitted. The implementation must be resilient to this fact.」也就是说,服务满了就让 call 返回的 future 报错。

#161 把契约改成现在的样子。PR 说明只有一句:「This updates the Service contract requiring poll_ready to be called before call. This allows Service::call to panic in the event the user of the service omits poll_ready or does not wait until Ready is observed.」同一个提交加上了规则 2(「repeated calls to poll_ready must return either Ready or Err」)和规则 3,并按新契约改了 balance、buffer、filter、in-flight-limit、mock、rate-limit、reconnect 这几个 crate。第一个包含它的 tower 版本是 0.1.0。

本页上面 ConcurrencyLimit::call 里的 expect 就是这次改动允许的写法:规则保证调用方先拿到了许可,call 不用再处理「没有许可」的情况。

第 4 条:call 可能永远不来

规则 4 是 2022 年才写进文档的(#662),但它约束的行为一直存在:调用方完全可以 ready().await 之后改主意,比如选了另一个服务、或者整个任务被取消。poll_ready 占到的东西必须放在会自动归还的地方。

后半句「the future returned by call is dropped before it is polled」同样重要:call 已经把资源移进了 future,但 future 一次都没被 poll 就丢掉了,资源也要回来。

❗IMPORTANT
动脑筋:call 了但从不 poll,许可回得来吗?
tower = { version = "=0.5.3", features = ["limit", "util"] }
tokio = { version = "=1.54.1", features = ["full"] }
use std::convert::Infallible;
use std::sync::Arc;
use tokio::sync::Semaphore;
use tower::limit::ConcurrencyLimit;
use tower::{service_fn, Service, ServiceExt};

#[tokio::main(flavor = "current_thread")]
async fn main() {
  let sem = Arc::new(Semaphore::new(1));
  let echo = service_fn(|req: u32| async move { Ok::<_, Infallible>(req) });
  let mut svc = ConcurrencyLimit::with_semaphore(echo, sem.clone());

  svc.ready().await.unwrap();
  println!("after ready: {}", sem.available_permits());
  let fut = svc.call(1);
  println!("after call:  {}", sem.available_permits());
  drop(svc);
  println!("after drop(svc): {}", sem.available_permits());
  drop(fut);
  println!("after drop(fut): {}", sem.available_permits());
}
四行输出里的数字分别是几?特别是第三行:服务已经 drop 了,许可回来了吗?

克隆之后不再就绪

Service 的文档有一节专门讲这件事,标题是 Be careful when cloning inner services:

/// Services are permitted to panic if `call` is invoked without obtaining `Poll::Ready(Ok(()))`
/// from `poll_ready`. You should therefore be careful when cloning services for example to move
/// them into boxed futures. Even though the original service is ready, the clone might not be.

中间件想在 call 里把内层服务移进一个 async 块时,很容易写成 let mut inner = self.inner.clone(); 再在块里 inner.call(req)。被 poll_ready 过的是 self.inner,克隆出来的副本没有就绪,就是上面实验里的 b。文档给的正确写法是用 mem::replace 把就绪的那个换出来,把克隆留在原地:

    fn call(&mut self, req: R) -> Self::Future {
        let clone = self.inner.clone();
        // take the service that was ready
        let mut inner = std::mem::replace(&mut self.inner, clone);
        Box::pin(async move {
            inner.call(req).await
        })
    }

留在 self.inner 里的克隆没有就绪状态,下一次请求之前调用方本来就要再 poll_ready,所以不会出问题。这个错误在实际项目里的样子放在第 09 部分(常见坑)。

📝NOTE
没有蠢问题

问: 为什么要把「能不能接」和「接」拆成两个方法?让 call 返回的 future 自己等到有空不就行了?

答: 那样调用方在知道服务满了之前就已经把请求交出去了,只能排队。拆开以后,调用方在交出请求之前就能得到「现在不行」这个信号,并决定怎么办:等(背压一路传回去)、立刻拒绝(LoadShed 在内层没就绪时让请求立刻以 Overloaded 错误结束,见 LoadShed)、或者换一个已经就绪的服务(负载均衡,见第 06 部分)。Service 文档的 Backpressure 一节也是这么说的:「The caller is responsible for ensuring that the service is ready to receive the request before calling it.」

Ready(Err) 之后

规则 3 约束的是调用方。tower 自己的调用方都照做:Oneshot 把错误原样返回、不再 call;call_all 在 poll_ready 报错后结束整条响应流,不再取请求。两者见 ServiceExt 与 Oneshot。

注意区分两种错误:poll_ready 返回的错误说的是服务坏了;call 返回的 future 解析出的 Err 说的是这一个请求失败了,服务还能接着用。

call 不在任务里

call 没有 cx 参数,拿不到 waker,所以它不能等任何东西,也不应该调 poll_ready。中间件的 call 通常只做三件事:从 self 里取出 poll_ready 时占到的资源,调内层的 call 拿到内层的 future,把两者包进自己的 ResponseFuture 返回。需要等的事(Timeout 的计时、Retry 的退避)都在返回的 future 里,由 poll 这个 future 的任务去等。

转发实现:&mut S 与 Box<S>

文件末尾有两个转发实现,&mut S 和 Box<S>:

impl<S, Request> Service<Request> for &mut S
where
    S: Service<Request> + ?Sized,
{
    type Response = S::Response;
    type Error = S::Error;
    type Future = S::Future;

    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), S::Error>> {
        (**self).poll_ready(cx)
    }

    fn call(&mut self, request: Request) -> S::Future {
        (**self).call(request)
    }
}

impl<S, Request> Service<Request> for Box<S>
where
    S: Service<Request> + ?Sized,
{
    // 同上
}

两者的关联类型都直接用 S 的,不多包一层。用处:

  • &mut S:消费 self 的组合子(oneshot、call_all、ready_oneshot)可以只借用服务,用完服务还在。ServiceExt::ready 返回的 Ready<'a, T, Request> 内部就是 ReadyOneshot<&'a mut T, Request>,靠的正是这个实现。
  • Box<S>:S: ?Sized 让 Box<dyn Service<...>> 本身也是 Service,类型擦除(第 03 部分的 BoxService 一族)建在这上面。
tower = { version = "=0.5.3", features = ["util"] }
tokio = { version = "=1.54.1", features = ["full"] }
use std::convert::Infallible;
use std::future::{ready, Ready};
use tower::{service_fn, Service, ServiceExt};

type Fut = Ready<Result<u32, Infallible>>;

#[tokio::main(flavor = "current_thread")]
async fn main() {
  let mut svc = service_fn(|req: u32| ready(Ok::<_, Infallible>(req * 10)));

  // &mut S is a Service: oneshot consumes the reference, not svc
  let r = (&mut svc).oneshot(1).await.unwrap();
  println!("via &mut: {r}");
  let r = svc.ready().await.unwrap().call(2).await.unwrap();
  println!("svc still usable: {r}");

  // Box<dyn Service> is a Service, because Box<S> allows S: ?Sized
  let mut boxed: Box<dyn Service<u32, Response = u32, Error = Infallible, Future = Fut>> =
    Box::new(svc);
  let r = boxed.ready().await.unwrap().call(3).await.unwrap();
  println!("via Box<dyn>: {r}");
}
via &mut: 10
svc still usable: 20
via Box<dyn>: 30

tag 里的源码和 crates.io 上的 0.3.3 不完全一样

tag tower-0.5.3 里的 tower-service 源码比 crates.io 上发布的 tower-service 0.3.3(2024-08,至今最新)多了两处改动,tower 0.5.3 实际依赖的是后者:

tag 源码crates.io 0.3.3
&mut S 的约束S: Service<Request> + ?Sizedimpl<'a, S, Request> Service<Request> for &'a mut S where S: Service<Request> + 'a,S 必须是 Sized
no_std#![no_std],用 core 和 alloc用 std

前者来自 #650(2025-07 合并),PR 说明是:「While Service impl for Box<S> covers unsized S case, the impl for &mut S is not, this PR is an attempt to fix that.」后者来自 #810。规则相关的文档文字两边一致。

所以用 crates.io 上的版本时,&mut dyn Service<...> 还不是 Service,而 Box<dyn Service<...>> 是:

type DynSvc = dyn Service<u32, Response = u32, Error = Infallible, Future = Ready<Result<u32, Infallible>>>;

fn needs_service<S: Service<u32>>(_svc: S) {}

fn pass_dyn(boxed: Box<DynSvc>, by_ref: &mut DynSvc) {
  needs_service(boxed); // Box<S>: S may be unsized
  needs_service(by_ref); // &mut S: S must be Sized in tower-service 0.3.3
}

rustc 1.99.0 只在第二行报错(后面列举其他实现的 help 省略):

error[E0277]: the trait bound `&mut _: Service<u32>` is not satisfied
  --> examples/mutdyn.rs:11:17
   |
11 |   needs_service(by_ref); // &mut S: S must be Sized in tower-service 0.3.3
   |   ------------- ^^^^^^ the trait `Sized` is not implemented for `dyn Service<u32, Error = Infallible, Future = _, Response = u32>`
   |   |
   |   required by a bound introduced by this call

动脑筋答案

M2 Max、rustc 1.99.0、--release,连跑 3 次输出相同:

after ready: 0
after call:  0
after drop(svc): 0
after drop(fut): 1
  • after ready:ready().await 让 poll_ready 取走了唯一的许可。
  • after call:call 把许可从 self.permit 移进了返回的 ResponseFuture,信号量里还是 0。
  • after drop(svc):服务没了,但许可不在服务里,在 fut 里,所以还是 0。这一步说明许可跟着请求走,而不是跟着服务走。
  • after drop(fut):future 一次都没 poll 就被丢掉,许可随它 drop,回到 1。这正是规则 4 的后半句。

小结

  • Service<Request> 只有 poll_ready 和 call;Response、Error、Future 是关联类型,中间件靠 S::Future 一层层包出请求主线。
  • poll_ready 四条规则:Pending 时负责唤醒;Ready(Ok) 之后到 call 之前保持就绪;Ready(Err) 表示实例报废,调用方应丢弃;预留的资源在没有 call、或 future 没被 poll 就 drop 时也要释放。
  • call 不在任务里,不能等、不能调 poll_ready;没拿到 Ready(Ok) 就 call,实现方可以 panic,ConcurrencyLimit 的信息是 max requests in-flight; poll_ready must be called first。
  • 克隆出来的服务不带就绪状态;在 call 里要用 mem::replace 换出就绪的那个。
  • &mut S 和 Box<S> 原样转发。tag 源码里两者都允许 ?Sized,crates.io 上的 tower-service 0.3.3 只有 Box<S> 允许。

💬 关联讨论

发起讨论
加载中...