Either 与 FutureService
主线位置:
Either在两条主线上都只是按分支转发,future 外面包一层二选一的枚举。FutureService只改就绪主线:服务还没造好时,poll_ready去推进那个造服务的 future;请求主线原样,Future就是造出来的服务的S::Future。
stateDiagram-v2 [*] --> Future: FutureService new Future --> Future: poll 得 Pending Future --> Service: poll 得 Ok svc Future --> Failed: poll 得 Err Service --> Service: 转发 poll_ready 和 call Failed --> [*]: 调用方应丢弃
两个类型都在 tower::util 里,都由 util feature 打开。它们放在一起讲,是因为都在回答同一个问题:手里的服务不是一个固定的具体类型时怎么办。上一页的答案是装箱;Either 在只有两种可能时不用装箱;FutureService 处理的是「服务现在还没有」。
Either:两种服务二选一
Either 就是一个两分支的枚举,Service 实现按分支转发:
#[derive(Clone, Copy, Debug)]
pub enum Either<A, B> {
#[allow(missing_docs)]
Left(A),
#[allow(missing_docs)]
Right(B),
}
impl<A, B, Request> Service<Request> for Either<A, B>
where
A: Service<Request>,
B: Service<Request, Response = A::Response, Error = A::Error>,
{
type Response = A::Response;
type Error = A::Error;
type Future = EitherResponseFuture<A::Future, B::Future>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
match self {
Either::Left(service) => service.poll_ready(cx),
Either::Right(service) => service.poll_ready(cx),
}
}
fn call(&mut self, request: Request) -> Self::Future {
match self {
Either::Left(service) => EitherResponseFuture {
kind: Kind::Left {
inner: service.call(request),
},
},
Either::Right(service) => EitherResponseFuture {
kind: Kind::Right {
inner: service.call(request),
},
},
}
}
}
- 约束只有一条:
B的Response、Error和A的相同。两边的Future可以不同,EitherResponseFuture把它们装进同一个类型。 EitherResponseFuture是一个结构体,里面是私有枚举Kind,两个变体都用pin_project_lite标成#[pin],poll时投影到当前分支。两种 future 都内联在枚举里,不装箱。- 分支在构造时定死。
Either没有在两个服务之间切换的逻辑,Left就永远是Left。要按请求选服务,用 Steer。
它还实现了 Layer:Either<LA, LB> 包 S 得到 Either<LA::Service, LB::Service>。ServiceBuilder::option_layer 就是用 Either<L, Identity> 表示「有这一层 / 没有这一层」;两边错误类型对不上时会出什么编译错误、怎么修,见 ServiceBuilder。
用 Either 代替装箱
上一页开头「分支返回不同的栈」的例子,两个分支的 Response 都是 String、Error 都是 BoxError,正好满足 Either 的约束,不用装箱:
tower = { version = "=0.5.3", features = ["limit", "timeout", "util"] }
tokio = { version = "=1.54.1", features = ["macros", "rt-multi-thread"] }
use std::time::Duration;
use tower::util::Either;
use tower::{service_fn, BoxError, Service, ServiceBuilder, ServiceExt};
async fn handle(req: String) -> Result<String, BoxError> {
Ok(req.to_uppercase())
}
fn build(strict: bool) -> impl Service<String, Response = String, Error = BoxError> {
if strict {
Either::Left(
ServiceBuilder::new()
.timeout(Duration::from_secs(1))
.concurrency_limit(10)
.service_fn(handle),
)
} else {
Either::Right(service_fn(handle))
}
}
#[tokio::main]
async fn main() {
for strict in [true, false] {
let r = build(strict).oneshot("hi".to_string()).await.unwrap();
println!("strict={strict}: {r}");
}
}
M2 Max、rustc 1.99.0、--release,连跑 3 次输出相同:
strict=true: HI
strict=false: HI
两个分支现在是同一个类型 Either<Timeout<ConcurrencyLimit<ServiceFn<..>>>, ServiceFn<..>>,impl Service 就能接受。和 BoxService 比:
Either | BoxService | |
|---|---|---|
| 分支数 | 2,更多要嵌套 Either<A, Either<B, C>> | 任意 |
| 每个请求的堆分配 | 0 | 1 |
| 类型能不能写出来 | 仍然带着两边的具体类型,闭包写不出来 | BoxService<T, U, E>,总能写出来 |
| 错误类型 | 两边必须相同,不做转换 | 由 new 的 E 决定,两边也要先统一 |
两者都要求两边错误类型一致:BoxService<T, U, E> 的 E 也是一个固定的类型,上一页的例子能编译,是因为 timeout 的错误类型本来就是 BoxError,handle 也返回 BoxError。
tower 内部也有叫 Either 的东西:filter 和 hedge 用的是 futures_util::future::Either,那是一个 future,不是服务,别和这里的 tower::util::Either 混在一起。
0.4 的 Either 变体叫 A / B,Service 实现要求 A::Error: Into<BoxError>、B::Error: Into<BoxError>,type Error = crate::BoxError;它本身还实现了 Future,同时充当响应 future。#637(2022-02 合并)改成了现在的样子,PR 说明给了两个理由。一是错误类型:「In practice I've found Either be hard to use since it changes the error type to BoxError. That means if you combine two infallible services you get a service that, to the type system, is fallible.」这和 axum 要求服务不能出错的模型冲突。二是依赖:「the fact that Either implements Future also means we cannot fully remove the dependency on pin-project since pin-project-lite doesn't support tuple enum variants, only named fields.」
所以现在的写法是:要求两边 Error 相同(「I hope this simpler model will lead to better compiler errors」),响应 future 拆成 EitherResponseFuture 加一个带命名字段的私有枚举 Kind(Left { inner }),好让 pin_project_lite 处理;Either 自己不再是 Future。这是破坏性改动,PR 里说要等 0.5,第一个包含它的版本是 tower 0.5.0。
FutureService:将来才有的服务
有时服务要异步才造得出来,比如先建一条连接。FutureService 把「一个会产出服务的 future」当成服务用:
#[derive(Clone)]
pub struct FutureService<F, S> {
state: State<F, S>,
}
#[derive(Clone)]
enum State<F, S> {
Future(F),
Service(S),
}
impl<F, S, R, E> Service<R> for FutureService<F, S>
where
F: Future<Output = Result<S, E>> + Unpin,
S: Service<R, Error = E>,
{
type Response = S::Response;
type Error = E;
type Future = S::Future;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
loop {
self.state = match &mut self.state {
State::Future(fut) => {
let fut = Pin::new(fut);
let svc = std::task::ready!(fut.poll(cx)?);
State::Service(svc)
}
State::Service(svc) => return svc.poll_ready(cx),
};
}
}
fn call(&mut self, req: R) -> Self::Future {
if let State::Service(svc) = &mut self.state {
svc.call(req)
} else {
panic!("FutureService::call was called before FutureService::poll_ready")
}
}
}
逐段看 poll_ready:
State::Future:poll 造服务的 future。Pending由ready!直接返回,waker 已经登记在那个 future 里;Err(e)由?返回Ready(Err(e));Ok(svc)就把状态换成State::Service,然后loop再转一圈。State::Service:直接返回svc.poll_ready(cx)。所以 future 完成的同一次poll_ready里,新服务就被问了一次;它没就绪,结果仍是Pending,这时 waker 登记在新服务那里。- 错误类型:future 的错误和服务的错误必须是同一个
E,因为两者都从poll_ready返回。
Unpin 约束:poll_ready 拿到的是 &mut self,不是 Pin<&mut Self>,所以没法把字段里的 future 安全地 pin 住,只能要求 F: Unpin,用 Pin::new。文档(第 96–103 行)给的办法是传 Box::pin(async { .. })。服务不需要 pin 的原因见 Service trait。
call:不在 State::Service 就 panic,信息是 FutureService::call was called before FutureService::poll_ready。请求主线上它什么也不包,Future = S::Future。
两个构造函数有一处差别:future_service 在构造时就要求 S: Service<R, Error = E>,FutureService::new 什么都不要求。后者是 #523 加的,理由是构造处的约束会逼调用方知道请求类型 R:「the caller must now either know the request type, or be generic over one.」在中间件里构造 FutureService 时,这意味着要多带一个 PhantomData。
下面的程序用 tower-test 的 mock 当「造出来的服务」(见 tower-test),一开始 allow(0),让它不就绪;造服务的 future 等一个 oneshot(见 oneshot、watch、broadcast、Notify)。
tower = { version = "=0.5.3", features = ["util"] }
tower-test = "0.4"
tokio = { version = "=1.54.1", features = ["sync"] }
tokio-test = "0.4"
use std::panic::{catch_unwind, AssertUnwindSafe};
use tokio::sync::oneshot;
use tokio_test::task;
use tower::util::FutureService;
use tower::Service;
use tower_test::mock;
fn main() {
std::panic::set_hook(Box::new(|_| {}));
let (svc, mut handle) = mock::pair::<u32, u32>();
handle.allow(0); // inner service: not ready yet
let (tx, rx) = oneshot::channel();
let mut fs = FutureService::new(Box::pin(async move {
let svc = rx.await.map_err(|_| "maker dropped")?;
Ok::<_, tower::BoxError>(svc)
}));
let mut t = task::spawn(());
let p = catch_unwind(AssertUnwindSafe(|| fs.call(1))).err().unwrap();
println!("call before ready: panic {:?}", p.downcast_ref::<&str>().unwrap());
println!("1 poll_ready: {:?}", t.enter(|cx, _| fs.poll_ready(cx)).map(|r| r.is_ok()));
tx.send(svc).ok().unwrap();
println!(" woken after send: {}", t.is_woken());
println!("2 poll_ready: {:?}", t.enter(|cx, _| fs.poll_ready(cx)).map(|r| r.is_ok()));
handle.allow(1);
println!(" woken after allow: {}", t.is_woken());
println!("3 poll_ready: {:?}", t.enter(|cx, _| fs.poll_ready(cx)).map(|r| r.is_ok()));
}
M2 Max、rustc 1.99.0、--release,连跑 3 次输出相同:
call before ready: panic "FutureService::call was called before FutureService::poll_ready"
1 poll_ready: Pending
woken after send: true
2 poll_ready: Pending
woken after allow: true
3 poll_ready: Ready(true)
- panic:一开始还在
State::Future,call直接 panic。 - 1:
oneshot里还没有服务,Pending,waker 登记在rx上。 - woken after send:
tx.send唤醒了这个任务。 - 2:future 完成,状态换成
State::Service,同一次调用里接着问 mock,mock 不就绪,仍是Pending。如果把FutureService理解成「future 完成前Pending、完成后Ready」,这一行就对不上:完成之后它的就绪完全取决于造出来的服务。 - woken after allow / 3:
allow(1)唤醒任务,再问就是Ready。
问: 我直接 let svc = make().await?; 再用不就行了,为什么要 FutureService?
答: .await 要求你此刻就在一个 async 上下文里、而且愿意在这里等。FutureService 让你立刻得到一个服务,把「等它造好」推迟到第一次 poll_ready,由真正要发请求的那个任务去等。这样它可以先被交给 ServiceBuilder 套上中间件、放进 Buffer、存进结构体,造服务的等待也走就绪主线,和其他背压一样处理。它只造一次;造失败或用着用着坏了要重新造,那是 Reconnect 的事。
future 报错之后
? 返回 Ready(Err(e)) 时,状态没有变,self.state 里还是那个已经完成了的 future。按 Service trait 的规则 3,poll_ready 报错表示这个实例报废,调用方应当丢掉它,FutureService 也就没有为「报错之后再问」做任何处理。
tower = { version = "=0.5.3", features = ["util"] }
use std::panic::{catch_unwind, AssertUnwindSafe};
use std::task::{Context, Waker};
use tower::util::{BoxService, FutureService};
use tower::{BoxError, Service};
type Svc = BoxService<u32, u32, BoxError>;
fn main() {
std::panic::set_hook(Box::new(|_| {})); // keep the default hook quiet
let mut cx = Context::from_waker(Waker::noop());
let mut fs: FutureService<_, Svc> =
FutureService::new(Box::pin(async { Err::<Svc, BoxError>("connect failed".into()) }));
for i in 1..=2 {
match catch_unwind(AssertUnwindSafe(|| fs.poll_ready(&mut cx))) {
Ok(r) => println!("poll_ready #{i}: {r:?}"),
Err(p) => println!("poll_ready #{i}: panic {:?}", p.downcast_ref::<&str>().unwrap()),
}
}
}
第一次 poll_ready 返回 Ready(Err("connect failed"))。第二次会怎样:再返回一次同样的错误、返回 Pending,还是别的?
动脑筋答案
M2 Max、rustc 1.99.0、--release,连跑 3 次输出相同:
poll_ready #1: Ready(Err("connect failed"))
poll_ready #2: panic "`async fn` resumed after completion"
- #1:造服务的 future 返回
Err,?把它变成Ready(Err(..)),状态仍是State::Future。 - #2:
poll_ready又去 poll 同一个 future,而它已经返回过结果了。async 块生成的状态机在完成后再被 poll 会 panic,信息是`async fn` resumed after completion(async 块也用这条信息)。换成别的 future,结果取决于它自己怎么处理完成后的 poll,FutureService不管。
所以 poll_ready 报错后要丢掉 FutureService。tower 自己的调用方(Oneshot、call_all)都是这样做的。
小结
Either按分支转发poll_ready和call,要求两边Response、Error相同;EitherResponseFuture用私有枚举包住两种 future,不装箱。- 只有两种可能时,
Either可以代替BoxService,每个请求省一次堆分配,代价是类型里仍带着两边的具体类型。 Either也是Layer,option_layer靠它;0.5 起不再把错误转成BoxError(#637)。FutureService在poll_ready里先推进造服务的 future,完成的同一次调用里就去问新服务;之后的就绪完全由新服务决定。F必须Unpin,通常传Box::pin(async { .. })。- 没就绪就
call会 panic;造服务失败后状态不变,再poll_ready会再次 poll 已完成的 future,必须丢弃实例。