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 的契约全写在注释里,编译器检查不了,后面每个中间件都按这几段注释来实现。
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 |
| 4 | poll_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再问一次就拿到了那个许可。
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 就丢掉了,资源也要回来。
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());
}
克隆之后不再就绪
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 部分(常见坑)。
问: 为什么要把「能不能接」和「接」拆成两个方法?让 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>
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> + ?Sized | impl<'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>允许。