Rust in Everything
基于 tower 0.5.3

ServiceBuilder

主线位置:装配阶段,决定两条主线经过各层的顺序。先写的 layer 在最外层:就绪主线上它最先被问到,请求主线上它最先拿到请求、最后拿到响应。
flowchart TB
  n["new():Identity"] --> a[".layer(A)"]
  a --> sa["Stack A Identity"]
  sa --> b[".layer(B)"]
  b --> sb["Stack B (Stack A Identity)"]
  sb --> s[".service(svc)"]
  s --> r["A 包着 B 包着 svc"]

builder/mod.rs 有 871 行,绝大部分是文档和一个个一行实现的快捷方法。真正的机制只有三处:一个字段、layer、service。

一个字段,一个 Stack

ServiceBuilder、new、layer、service:

#[derive(Clone)]
pub struct ServiceBuilder<L> {
    layer: L,
}

impl ServiceBuilder<Identity> {
    pub const fn new() -> Self {
        ServiceBuilder {
            layer: Identity::new(),
        }
    }
}

impl<L> ServiceBuilder<L> {
    pub fn layer<T>(self, layer: T) -> ServiceBuilder<Stack<T, L>> {
        ServiceBuilder {
            layer: Stack::new(layer, self.layer),
        }
    }

    // ...

    pub fn service<S>(&self, service: S) -> L::Service
    where
        L: Layer<S>,
    {
        self.layer.layer(service)
    }
}

Stack::new(layer, self.layer):新 layer 放在 Stack 的 Inner 位置,之前攒下的整串放在 Outer 位置。Stack 先用 Inner 包,再用 Outer 包,所以最后写的 layer 离服务最近,最先写的在最外层。

这和 ServiceBuilder 文档里 Order 一节说的一致:

/// The order in which layers are added impacts how requests are handled. Layers
/// that are added first will be called with the request first. The argument to
/// `service` will be last to see the request.

文档接着用 buffer 和 concurrency_limit 举例:.buffer(100).concurrency_limit(10) 时 buffer 在外,100 个排队位置加上已经交给下一层的 10 个,最多 110 个请求在途;反过来写成 .concurrency_limit(10).buffer(100),concurrency_limit 在外,总共只有 10 个。

用两个日志层验证

写一个 LogLayer,在 poll_ready、call 和响应完成时各打印一行,然后用 type_name_of_val 看类型:

tower = { version = "=0.5.3", features = ["timeout", "limit", "util"] }
tokio = { version = "=1.54.1", features = ["full"] }
use std::any::type_name_of_val;
use std::convert::Infallible;
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use tower::{service_fn, Layer, Service, ServiceBuilder, ServiceExt};

#[derive(Clone, Debug)]
struct LogLayer(&'static str);

struct Log<S> {
  name: &'static str,
  inner: S,
}

impl<S> Layer<S> for LogLayer {
  type Service = Log<S>;
  fn layer(&self, inner: S) -> Log<S> {
    Log { name: self.0, inner }
  }
}

impl<S, R> Service<R> for Log<S>
where
  S: Service<R>,
  S::Future: 'static,
{
  type Response = S::Response;
  type Error = S::Error;
  type Future = Pin<Box<dyn Future<Output = Result<S::Response, S::Error>>>>;

  fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), S::Error>> {
    println!("{} poll_ready", self.name);
    self.inner.poll_ready(cx)
  }

  fn call(&mut self, req: R) -> Self::Future {
    println!("{} call", self.name);
    let name = self.name;
    let fut = self.inner.call(req);
    Box::pin(async move {
      let res = fut.await;
      println!("{} response", name);
      res
    })
  }
}

#[tokio::main(flavor = "current_thread")]
async fn main() {
  let builder = ServiceBuilder::new().layer(LogLayer("A")).layer(LogLayer("B"));
  println!("{:?}", builder);
  let svc = builder.service(service_fn(|req: u32| async move {
    println!("inner handles {req}");
    Ok::<_, Infallible>(req)
  }));
  println!("{}", type_name_of_val(&builder));
  println!("{}", type_name_of_val(&svc));
  svc.oneshot(7).await.unwrap();

  let b2 = ServiceBuilder::new()
    .timeout(std::time::Duration::from_secs(1))
    .concurrency_limit(10);
  let s2 = b2.service(service_fn(|req: u32| async move { Ok::<_, Infallible>(req) }));
  println!("{}", type_name_of_val(&b2));
  println!("{}", type_name_of_val(&s2));
}

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

ServiceBuilder(Identity, LogLayer("A"), LogLayer("B"))
tower::builder::ServiceBuilder<tower_layer::stack::Stack<order::LogLayer, tower_layer::stack::Stack<order::LogLayer, tower_layer::identity::Identity>>>
order::Log<order::Log<tower::util::service_fn::ServiceFn<order::main::{{closure}}::{{closure}}>>>
A poll_ready
B poll_ready
A call
B call
inner handles 7
B response
A response
tower::builder::ServiceBuilder<tower_layer::stack::Stack<tower::limit::concurrency::layer::ConcurrencyLimitLayer, tower_layer::stack::Stack<tower::timeout::layer::TimeoutLayer, tower_layer::identity::Identity>>>
tower::timeout::Timeout<tower::limit::concurrency::service::ConcurrencyLimit<tower::util::service_fn::ServiceFn<order::main::{{closure}}::{{closure}}>>>
  • Debug:Identity, A, B,按添加顺序。这是 Stack 的 Debug 先打 outer 再打 inner 的结果。
  • 两条主线:A poll_ready 在 B poll_ready 前面(就绪往里传);A call 在 B call 前面,B response 在 A response 前面(请求往里送、响应往外解)。
  • 类型:后两行最清楚。builder 的类型是 Stack<ConcurrencyLimitLayer, Stack<TimeoutLayer, Identity>>,后写的 ConcurrencyLimitLayer 在最左边;包出来的服务是 Timeout<ConcurrencyLimit<ServiceFn<..>>>,先写的 Timeout 在最外层。两个类型名里的顺序正好相反。

axum 的 Router::layer 是反过来的:后加的在外层,这个坑放在第 09 部分(常见坑)。

service 拿的是 &self

service 的接收者是 &self,同一个 builder 可以包任意多个服务。

📝NOTE
冷知识:service 原来会吃掉 builder

0.4 之前,ServiceBuilder::service 的签名是 pub fn service<S>(self, service: S) -> L::Service,一个配好的 builder 只能包一个服务。issue #501 指出:「the Layer::layer method only needs an &self receiver --- the layers themselves can be reused. All ServiceBuilder::service does is call Layer::layer, so there's no real reason for service to move self.」改成 &self 以后,「the same middleware could be applied to any number of services」,比如给 MakeService 造出来的每个服务都套同一组中间件。

#504 只改了这一个接收者,从 tower 0.4.0 开始生效。

ServiceBuilder<L> 本身也实现了 Layer(第 862 行,#600 加入,tower 0.4.9 起),转发给里面的 L,所以一个 builder 可以当作一层塞进另一个 builder,或者交给任何接受 Layer 的 API。

❗IMPORTANT
动脑筋:builder 套 builder

LogLayer 和上面的实验相同,依赖 features = ["util"]。

#[tokio::main(flavor = "current_thread")]
async fn main() {
  let inner = ServiceBuilder::new().layer(LogLayer("B")).layer(LogLayer("C"));
  let builder = ServiceBuilder::new()
    .layer(LogLayer("A"))
    .layer(inner)
    .layer(LogLayer("D"));
  println!("{:?}", builder);
  let svc = builder.service(service_fn(|req: u32| async move { Ok::<_, Infallible>(req) }));
  svc.oneshot(7).await.unwrap();
}

四行 call 的顺序是什么?

快捷方法

除了 layer,其余添加 layer 的方法都是 self.layer(XxxLayer::new(..)) 的一行包装,各自挂在对应模块的 feature 上:

方法加的 layerfeature
option_layerEither<T, Identity>util
layer_fnLayerFn<F>无
bufferBufferLayer<Request>buffer
concurrency_limitConcurrencyLimitLayerlimit
load_shedLoadShedLayerload-shed
rate_limitRateLimitLayerlimit
retryRetryLayer<P>retry
timeoutTimeoutLayertimeout
filterFilterLayer<P>filter
filter_asyncAsyncFilterLayer<P>filter
map_requestMapRequestLayer<F>util
map_responseMapResponseLayer<F>util
map_errMapErrLayer<F>util
map_futureMapFutureLayer<F>util
thenThenLayer<F>util
and_thenAndThenLayer<F>util
map_resultMapResultLayer<F>util
boxed把服务装进 BoxService 的 LayerFnutil
boxed_clone把服务装进 BoxCloneService 的 LayerFnutil
boxed_clone_syncBoxCloneSyncServiceLayer,见下util

另外三个不加 layer 的:into_inner 取出攒好的 L;service;service_fn(util)等于 self.service(service_fn(f))。

几点:

  • spawn_ready、reconnect、hedge、负载均衡这些没有快捷方法,用 .layer(...) 加。
  • buffer 带一个类型参数 Request,因为 BufferLayer<Request> 要知道通道里传什么。
  • boxed_clone_sync(tower 0.5.3 新增,#804)和另外两个装箱方法不一样:它先 self.into_inner() 拿出整串 layer,交给 BoxCloneSyncServiceLayer::new,返回 ServiceBuilder::new().layer(...),类型是 Stack<BoxCloneSyncServiceLayer<..>, Identity>,之前的整串都被擦掉了。装箱的细节放在第 03 部分。

option_layer:Either<T, Identity>

想按配置决定加不加某一层,又不想让两种情况变成两个类型,用 option_layer。它调的是 util::option_layer:

pub fn option_layer<L>(layer: Option<L>) -> Either<L, Identity> {
    if let Some(layer) = layer {
        Either::Left(layer)
    } else {
        Either::Right(Identity::new())
    }
}

Either 同时实现了 Layer(第 90 行)和 Service(第 27 行)。作为 layer,Either<TimeoutLayer, Identity> 包 S 得到 Either<Timeout<S>, S>;作为服务,它要求两边的响应和错误类型相同:

impl<A, B, Request> Service<Request> for Either<A, B>
where
    A: Service<Request>,
    B: Service<Request, Response = A::Response, Error = A::Error>,

这就带来一个常见的类型错误:Timeout<S> 的错误类型是 BoxError,S 自己的错误类型往往不是,于是 Either<Timeout<S>, S> 不是 Service。下一节就用它做例子。

check_service 一族:只在编译期起作用

check_clone、check_service_clone、check_service 的函数体都只有一个 self,全部意义在 where 子句上:

    #[inline]
    pub fn check_service<S, T, U, E>(self) -> Self
    where
        L: Layer<S>,
        L::Service: Service<T, Response = U, Error = E>,
    {
        self
    }
方法断言
check_clone()这个 builder 本身是 Clone
check_service_clone::<S>()到这一步为止的 layer 包 S 得到的服务是 Clone
check_service::<S, T, U, E>()到这一步为止的 layer 包 S 得到的服务是 Service<T, Response = U, Error = E>

它们不改变 builder,也不产生运行时代码。用处在于:Layer 没有任何约束(见 Layer 与 Stack),一串 layer 拼错了,编译器要等到这个服务被用的时候才发现,报错落在用的那一行,带着整串类型。在中间插一个 check_service,错误就落在插入的那一行。

下面的 Echo 是一个 Service<String>,错误类型是 Infallible。先不加检查:

tower = { version = "=0.5.3", features = ["timeout", "util"] }
tokio = { version = "=1.54.1", features = ["full"] }
use std::convert::Infallible;
use std::future::{ready, Ready};
use std::task::{Context, Poll};
use std::time::Duration;
use tower::timeout::TimeoutLayer;
use tower::{Service, ServiceBuilder, ServiceExt};

struct Echo;

impl Service<String> for Echo {
  type Response = String;
  type Error = Infallible;
  type Future = Ready<Result<String, Infallible>>;
  fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Infallible>> {
    Poll::Ready(Ok(()))
  }
  fn call(&mut self, req: String) -> Self::Future {
    ready(Ok(req))
  }
}

#[tokio::main]
async fn main() {
  let timeout = Some(TimeoutLayer::new(Duration::from_secs(1)));
  let svc = ServiceBuilder::new()
    .option_layer(timeout)
    .map_request(|req: String| req.to_uppercase())
    .service(Echo);
  let rsp = svc.oneshot("hi".to_string()).await;
  println!("{rsp:?}");
}

rustc 1.99.0 的报错开头(中间一段指向 either.rs 第 20 行 pub enum Either<A, B> 的源码片段省略,它标注这个 Either 不满足 Service<_> 和 ServiceExt<_>;结尾还有两条同样形状的 &Either<...>、&mut Either<...> 和长类型写入文件的提示,也省略):

error[E0599]: the method `oneshot` exists for enum `Either<Timeout<MapRequest<Echo, {closure@...}>>, _>`, but its trait bounds were not satisfied
  --> examples/nocheck.rs:29:17
   |
29 |   let rsp = svc.oneshot("hi".to_string()).await;
   |                 ^^^^^^^ method cannot be called due to unsatisfied trait bounds
   |
   = note: the following trait bounds were not satisfied:
           `Either<tower::timeout::Timeout<MapRequest<Echo, {closure@examples/nocheck.rs:27:18: 27:31}>>, MapRequest<Echo, {closure@examples/nocheck.rs:27:18: 27:31}>>: Service<_>`
           which is required by `Either<tower::timeout::Timeout<MapRequest<Echo, {closure@examples/nocheck.rs:27:18: 27:31}>>, MapRequest<Echo, {closure@examples/nocheck.rs:27:18: 27:31}>>: ServiceExt<_>`

错误落在 oneshot 那一行,只说 Either<...> 不是 Service,没说为什么。在 option_layer 后面加一行:

    .option_layer(timeout)
    .check_service::<Echo, String, String, tower::BoxError>()

新的第一条错误(末尾指向 builder/mod.rs 源码的片段省略;oneshot 那一条错误仍然在,也省略):

error[E0271]: type mismatch resolving `<Echo as Service<String>>::Error == Box<dyn Error + Send + Sync>`
   --> examples/withcheck.rs:27:6
    |
 27 |     .check_service::<Echo, String, String, tower::BoxError>()
    |      ^^^^^^^^^^^^^ type mismatch resolving `<Echo as Service<String>>::Error == Box<dyn Error + Send + Sync>`
    |
note: expected this to be `Box<(dyn std::error::Error + Send + Sync + 'static)>`
   --> examples/withcheck.rs:12:16
    |
 12 |   type Error = Infallible;
    |                ^^^^^^^^^^
    = note: expected struct `Box<(dyn std::error::Error + Send + Sync + 'static)>`
                 found enum `Infallible`
    = note: required for `Either<tower::timeout::Timeout<Echo>, Echo>` to implement `Service<String>`
note: required by a bound in `ServiceBuilder::<L>::check_service`

现在错误指向了原因:Either 的右边是 Echo,它的 Error 是 Infallible,而左边 Timeout<Echo> 的是 BoxError。修法是在 option_layer 下面(更靠近服务的位置)先把错误统一成 BoxError,让 Either 两边都包同一个 MapErr<..>:

  for timeout in [Some(TimeoutLayer::new(Duration::from_secs(1))), None] {
    let svc = ServiceBuilder::new()
      .option_layer(timeout)
      .map_err(tower::BoxError::from)
      .check_service::<Echo, String, String, tower::BoxError>()
      .map_request(|req: String| req.to_uppercase())
      .service(Echo);
    let variant = match &svc {
      Either::Left(_) => "Left",
      Either::Right(_) => "Right",
    };
    let rsp = svc.oneshot("hi".to_string()).await;
    println!("{variant}: {rsp:?}");
  }
Left: Ok("HI")
Right: Ok("HI")

Some 和 None 两种情况是同一个类型,同一个循环变量装得下。

check_service_clone 的用法相同。比如在 rate_limit 后面检查:

  let _builder = ServiceBuilder::new()
    .concurrency_limit(10)
    .check_service_clone::<Echo>()
    .rate_limit(100, Duration::from_secs(1))
    .check_service_clone::<Echo>();

第一个检查通过,第二个报 the trait bound `ConcurrencyLimit<RateLimit<Echo>>: Clone` is not satisfied:ConcurrencyLimit<T> 只在 T: Clone 时才是 Clone,而 RateLimit 没有实现 Clone(见 RateLimit)。

📝NOTE
冷知识:这几个方法是照着 linkerd2-proxy 加的

check_clone、check_service_clone、check_service 是 #576 一起加的,从 tower 0.4.7 开始有。PR 说明写的是:作者在做一个套了大约 10 层中间件的 HTTP 客户端,「That sometimes produces some otherworldly type errors that can be hard to debug.」他在 linkerd2-proxy 里见过类似的 type check 方法,觉得 tower 也该有。

上面的实验就是这个场景的缩小版:没有检查时,错误落在使用处,只说 Either<...> 不满足 Service<_>;插一个 check_service,编译器就指出是 Echo 的 Error 对不上。

动脑筋答案

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

ServiceBuilder(Identity, LogLayer("A"), ServiceBuilder(Identity, LogLayer("B"), LogLayer("C")), LogLayer("D"))
A poll_ready
B poll_ready
C poll_ready
D poll_ready
A call
B call
C call
D call
D response
C response
B response
A response
  • call 的顺序是 A、B、C、D,就是书写顺序。inner 这个 builder 本身是一个 Layer,在外层 builder 里占 A 和 D 之间的位置;它内部 B 先写,所以 B 在 C 外面。
  • 第一行的 Debug 也是按书写顺序展开的,内层 builder 打印成一个嵌套的 ServiceBuilder(...)。
  • 响应从最里面的 D 开始往外解开。

小结

  • ServiceBuilder<L> 只有一个字段。.layer(x) 返回 ServiceBuilder<Stack<x, L>>,新 layer 在 Inner 位置,所以先写的在最外层,最先收到 poll_ready 和请求,最后拿到响应。
  • 类型名里的顺序和包出来的顺序相反:Stack<ConcurrencyLimitLayer, Stack<TimeoutLayer, Identity>> 包出 Timeout<ConcurrencyLimit<S>>。
  • service 拿 &self,builder 可重复使用;builder 本身也是 Layer。
  • 快捷方法都是 self.layer(XxxLayer::new(..)),按 feature 开关;boxed_clone_sync 会把之前的整串擦掉。
  • option_layer 给出 Either<T, Identity>,两种情况同一个类型,但 Either 要求两边的响应和错误类型一致。
  • check_clone、check_service_clone、check_service 只有 where 子句,用来把类型错误定位到出错的那一层。

💬 关联讨论

发起讨论
加载中...