Rust in Everything
基于 tower 0.5.3

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 比:

EitherBoxService
分支数2,更多要嵌套 Either<A, Either<B, C>>任意
每个请求的堆分配01
类型能不能写出来仍然带着两边的具体类型,闭包写不出来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 混在一起。

📝NOTE
冷知识:Either 以前会把错误变成 BoxError

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。
📝NOTE
没有蠢问题

问: 我直接 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 也就没有为「报错之后再问」做任何处理。

❗IMPORTANT
动脑筋:造服务失败后再问一次 poll_ready
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,必须丢弃实例。

💬 关联讨论

发起讨论
加载中...