Rust in Everything
基于 tower 0.5.3

类型擦除

主线位置:两条主线都穿过盒子。就绪主线上 poll_ready 经一次虚调用原样转发;请求主线上内层的 future 被 Box::pin 装箱,Timeout<ConcurrencyLimit<..>> 这样的长类型收成 BoxService<T, U, E>。

flowchart TB
  stack["具体的服务栈 S"] -->|"map_future(Box::pin)"| unified["future 统一装箱"]
  unified -->|"Box::new"| dynsvc["dyn Service + Send"]
  dynsvc -->|"SyncWrapper::new"| bs["BoxService T U E"]
  bs -->|"poll_ready"| vcall[虚调用,原样转发]
  bs -->|"call"| heap[每个请求装箱一次]

上一页说过,tower 的组合子和中间件一个都不装箱,每一层都把内层的类型写进自己的类型里。这让一摞中间件没有额外开销,也让它的类型又长又具体。大多数时候这不是问题,编译器替你推导;但有几种情况必须把类型「擦掉」。

分支返回不同的栈:编译器怎么说

最常见的一种:根据配置决定要不要加几层中间件。

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::{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 {
    ServiceBuilder::new()
      .timeout(Duration::from_secs(1))
      .concurrency_limit(10)
      .service_fn(handle)
  } else {
    service_fn(handle)
  }
}

rustc 1.99.0 的报错(两行指向长类型文件路径的 note 省略):

error[E0308]: `if` and `else` have incompatible types
  --> examples/branch.rs:15:5
   |
 9 | /    if strict {
10 | |/     ServiceBuilder::new()
11 | ||       .timeout(Duration::from_secs(1))
12 | ||       .concurrency_limit(10)
13 | ||       .service_fn(handle)
   | ||_________________________- expected because of this
14 | |    } else {
15 | |      service_fn(handle)
   | |      ^^^^^^^^^^^^^^^^^^ expected `Timeout<ConcurrencyLimit<ServiceFn<_>>>`, found `ServiceFn<fn(String) -> _ {handle}>`
16 | |    }
   | |____- `if` and `else` have incompatible types
   |
   = note: expected struct `tower:...le}>>>`
              found struct `ServiceFn<fn(String) -> impl std::future::Future<Output = Result<String, Box<(dyn std::error::Error + Send + Sync + 'static)>>> {handle}>`
help: you could change the return type to be a boxed trait object
   |
 8 - fn build(strict: bool) -> impl Service<String, Response = String, Error = BoxError> {
 8 + fn build(strict: bool) -> Box<dyn Service<String, Response = String, Error = BoxError>> {
   |

两个分支都是 Service<String, Response = String, Error = BoxError>,但 impl Trait 要求整个函数只返回一个具体类型,而一个是 Timeout<ConcurrencyLimit<ServiceFn<_>>>,一个是 ServiceFn<_>。

照 rustc 的建议把返回类型改成 Box<dyn Service<..>>、两个分支各包一个 Box::new,会得到第二个错误:

error[E0191]: the value of the associated type `Future` in `Service<String>` must be specified
 --> examples/branch_dyn.rs:8:35
  |
8 | fn build(strict: bool) -> Box<dyn Service<String, Response = String, Error = BoxError>> {
  |                                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |
help: specify the associated type
  |
8 | fn build(strict: bool) -> Box<dyn Service<String, Response = String, Error = BoxError, Future = /* Type */>> {
  |                                                                                      +++++++++++++++++++++

trait object 必须写出所有关联类型,而两个分支的 Future 也不一样:一个是 timeout::ResponseFuture<concurrency::ResponseFuture<..>>,一个是 handle 返回的匿名 future。光把服务装箱不够,future 也得装箱,统一成一个能写出来的类型。这正是 BoxService 做的事。改成:

use tower::util::BoxService;

fn build(strict: bool) -> BoxService<String, String, BoxError> {
  if strict {
    ServiceBuilder::new()
      .timeout(Duration::from_secs(1))
      .concurrency_limit(10)
      .service_fn(handle)
      .boxed()
  } else {
    service_fn(handle).boxed()
  }
}

#[tokio::main]
async fn main() {
  for strict in [true, false] {
    let r = build(strict).oneshot("hi".to_string()).await.unwrap();
    println!("strict={strict}: {r}");
  }
}
strict=true: HI
strict=false: HI

同类的场景还有两个:

  • 存进结构体字段或集合。 字段类型要写得出来,而闭包的类型写不出来。BoxService 文档的例子注释就是「Respond to requests using a closure, but closures cannot be named...」。Steer 的文档例子把两个不同的服务放进一个 Vec,也是先各自 BoxService::new,见 Steer 与 MakeService。
  • 只有两个分支、又不想装箱。 用 Either,见 Either 与 FutureService。

BoxService 怎么擦

BoxService 的字段、new 和 Service 实现:

pub struct BoxService<T, U, E> {
    inner:
        SyncWrapper<Box<dyn Service<T, Response = U, Error = E, Future = BoxFuture<U, E>> + Send>>,
}

type BoxFuture<T, E> = Pin<Box<dyn Future<Output = Result<T, E>> + Send>>;

impl<T, U, E> BoxService<T, U, E> {
    pub fn new<S>(inner: S) -> Self
    where
        S: Service<T, Response = U, Error = E> + Send + 'static,
        S::Future: Send + 'static,
    {
        // rust can't infer the type
        let inner: Box<dyn Service<T, Response = U, Error = E, Future = BoxFuture<U, E>> + Send> =
            Box::new(inner.map_future(|f: S::Future| Box::pin(f) as _));
        let inner = SyncWrapper::new(inner);
        BoxService { inner }
    }
    // ...
}

impl<T, U, E> Service<T> for BoxService<T, U, E> {
    type Response = U;
    type Error = E;
    type Future = BoxFuture<U, E>;

    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), E>> {
        self.inner.get_mut().poll_ready(cx)
    }

    fn call(&mut self, request: T) -> BoxFuture<U, E> {
        self.inner.get_mut().call(request)
    }
}

三步:

  1. map_future(|f| Box::pin(f) as _) 把 S 变成一个 Future = BoxFuture<U, E> 的服务。这样 trait object 的四个类型参数(T、U、E、Future)全都写得出来,E0191 那个问题就解决了。map_future 见 service_fn 与 map 一族。
  2. Box::new 把服务本身放上堆,擦成 dyn Service<..> + Send。服务只在这里分配一次。
  3. 包进 SyncWrapper,下一节讲。

S: Send + 'static、S::Future: Send + 'static 是这几步的要求:dyn .. + Send 要求服务 Send,BoxFuture 要求 future Send,trait object 默认 'static。ServiceExt::boxed、ServiceBuilder::boxed、BoxService::layer() 都只是转调 BoxService::new。

Service::Future 变成了 Pin<Box<dyn Future>>,所以每个请求都要分配一次堆内存,下面的动脑筋来数一数。

❗IMPORTANT
动脑筋:1000 次调用,分配几次?

用一个计数的全局分配器,对同一摞 map_request / map_response / map_err 各调 1000 次(每次 poll_ready、call、把 future poll 到完成),分别不装箱、boxed()、boxed_clone()。服务本身在计数开始前就建好了。

tower = { version = "=0.5.3", features = ["util"] }
use std::alloc::{GlobalAlloc, Layout, System};
use std::convert::Infallible;
use std::future::{ready, Future};
use std::pin::pin;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::task::{Context, Poll};
use tower::{service_fn, Service, ServiceBuilder, ServiceExt};

struct Counting;
static ALLOCS: AtomicUsize = AtomicUsize::new(0);

unsafe impl GlobalAlloc for Counting {
  unsafe fn alloc(&self, l: Layout) -> *mut u8 {
    ALLOCS.fetch_add(1, Ordering::Relaxed);
    unsafe { System.alloc(l) }
  }
  unsafe fn dealloc(&self, p: *mut u8, l: Layout) {
    unsafe { System.dealloc(p, l) }
  }
}

#[global_allocator]
static GLOBAL: Counting = Counting;

// poll_ready + call + poll the future to completion, 1000 times
fn run<S: Service<u32, Response = u32>>(name: &str, mut svc: S) {
  let waker = std::task::Waker::noop();
  let mut cx = Context::from_waker(waker);
  let before = ALLOCS.load(Ordering::Relaxed);
  for i in 0..1000 {
    assert!(matches!(svc.poll_ready(&mut cx), Poll::Ready(Ok(()))));
    let fut = pin!(svc.call(i));
    assert!(matches!(fut.poll(&mut cx), Poll::Ready(Ok(_))));
  }
  let n = ALLOCS.load(Ordering::Relaxed) - before;
  println!("{name:<28} {n} allocations / 1000 calls");
}

fn main() {
  let echo = service_fn(|x: u32| ready(Ok::<u32, Infallible>(x)));
  let stack = || {
    ServiceBuilder::new()
      .map_request(|x: u32| x + 1)
      .map_response(|x: u32| x * 2)
      .map_err(|e: Infallible| e)
      .service(echo)
  };
  run("stack", stack());
  run("stack.boxed()", stack().boxed());
  run("stack.boxed_clone()", stack().boxed_clone());
}
三行各是多少次?

Sync 从哪来:SyncWrapper

dyn Service<..> + Send 只有 Send,没有 Sync。可是 BoxService 的类型里没有任何对 Sync 的要求,它却是 Sync 的,文件末尾还有一个测试专门检查这件事(is_sync)。来源是 sync_wrapper crate(tower 0.5.3 用的是 1.0.2)的一行:

unsafe impl<T> Sync for SyncWrapper<T> {}

无条件 Sync,对 T 没有任何要求。它之所以安全,是因为 SyncWrapper 只提供 get_mut(&mut self)、get_pin_mut、into_inner(self),没有任何通过 &self 拿到内部的方法。Sync 的意思是「&Self 可以跨线程共享」,而一个 &SyncWrapper<T> 什么也做不了,共享它不会出事。

Service 的两个方法都是 &mut self,BoxService 正好只需要 get_mut。所以即使内层服务本身不是 Sync,BoxService 也是:装箱之后,服务里哪怕有一个 Cell,BoxService 也能放进要求 Sync 的地方。

下面的程序用一个带 Cell 的服务(Send、Clone,但不是 Sync)分别装进三种盒子,检查结果是不是 Sync:

tower = { version = "=0.5.3", features = ["util"] }
use std::cell::Cell;
use std::convert::Infallible;
use std::future::{ready, Ready};
use std::task::{Context, Poll};
use tower::util::{BoxCloneService, BoxCloneSyncService, BoxService};
use tower::Service;

// Send + Clone, but not Sync: Cell
#[derive(Clone)]
struct Counter {
  n: Cell<u32>,
}

impl Service<()> for Counter {
  type Response = u32;
  type Error = Infallible;
  type Future = Ready<Result<u32, Infallible>>;
  fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Infallible>> {
    Poll::Ready(Ok(()))
  }
  fn call(&mut self, _: ()) -> Self::Future {
    self.n.set(self.n.get() + 1);
    ready(Ok(self.n.get()))
  }
}

fn assert_sync<T: Sync>(_: &T) {}

fn main() {
  let c = Counter { n: Cell::new(0) };
  assert_sync(&BoxService::new(c.clone()));
  assert_sync(&BoxCloneService::new(c.clone()));
  let _ = BoxCloneSyncService::new(c);
}

rustc 1.99.0 报两个错(注册表和工具链路径缩成 ...,长类型文件的两行 note 省略),第 31 行的 BoxService 没有报错:

error[E0277]: `dyn CloneService<(), Error = _, Future = _, Response = u32> + Send` cannot be shared between threads safely
   --> examples/sync.rs:32:15
    |
 32 |   assert_sync(&BoxCloneService::new(c.clone()));
    |   ----------- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `dyn CloneService<(), Error = _, Future = _, Response = u32> + Send` cannot be shared between threads safely
    |   |
    |   required by a bound introduced by this call
    |
    = help: the trait `Sync` is not implemented for `dyn CloneService<(), Error = _, Future = _, Response = u32> + Send`
    = note: required for `Unique<_>` to implement `Sync`
note: required because it appears within the type `Box<_>`
   --> .../library/alloc/src/boxed.rs:236:12
    |
236 | pub struct Box<
    |            ^^^
note: required because it appears within the type `BoxCloneService<(), u32, Infallible>`
   --> .../tower-0.5.3/src/util/boxed_clone.rs:58:12
    |
 58 | pub struct BoxCloneService<T, U, E>(
    |            ^^^^^^^^^^^^^^^
note: required by a bound in `assert_sync`
   --> examples/sync.rs:27:19
    |
 27 | fn assert_sync<T: Sync>(_: &T) {}
    |                   ^^^^ required by this bound in `assert_sync`

error[E0277]: `Cell<u32>` cannot be shared between threads safely
  --> examples/sync.rs:33:36
   |
33 |   let _ = BoxCloneSyncService::new(c);
   |           ------------------------ ^ `Cell<u32>` cannot be shared between threads safely
   |           |
   |           required by a bound introduced by this call
   |
   = help: within `Counter`, the trait `Sync` is not implemented for `Cell<u32>`
   = note: if you want to do aliasing and mutation between multiple threads, use `std::sync::RwLock` or `std::sync::atomic::AtomicU32` instead
note: required because it appears within the type `Counter`
  --> examples/sync.rs:10:8
   |
10 | struct Counter {
   |        ^^^^^^^
note: required by a bound in `BoxCloneSyncService::<T, U, E>::new`
  --> .../tower-0.5.3/src/util/boxed_clone_sync.rs:29:65
   |
27 |     pub fn new<S>(inner: S) -> Self
   |            --- required by a bound in this associated function
28 |     where
29 |         S: Service<T, Response = U, Error = E> + Clone + Send + Sync + 'static,
   |                                                                 ^^^^ required by this bound in `BoxCloneSyncService::<T, U, E>::new`

三种结果对应三种做法:

  • BoxService:SyncWrapper 让它无条件 Sync。
  • BoxCloneService:盒子里是 dyn CloneService<..> + Send,没有 + Sync,所以它永远不是 Sync,与内层是否 Sync 无关。它也不能借 SyncWrapper 的办法:Clone::clone 只有 &self,而 SyncWrapper 恰恰不给 &self 访问。
  • BoxCloneSyncService:把 + Sync 写进 trait object,代价是 new 要求内层 S: Sync,带 Cell 的服务在构造时就被拒绝。
📝NOTE
冷知识:BoxService 曾经不是 Sync

#702 之前,BoxService 的字段就是 Box<dyn Service<..> + Send>,没有 Sync。issue #697 指出这没有必要:「This type doesn't have any methods accessing the inner value with &self, so by SyncWrapper logic it can be unconditionally Sync — however currently, it never implements the trait.」并列出三个选项:加一个 sync_wrapper 依赖、在 tower 里写 unsafe、等 SyncWrapper 进标准库。同一个 issue 里也写了为什么 BoxCloneService 不行:「this doesn't apply to BoxCloneService, since that type needs Clone」。

#702(2022-11 合并)选了第一个:引入 sync_wrapper,把字段换成 SyncWrapper<Box<..>>,poll_ready 和 call 改成 self.inner.get_mut(),就是上面引用的代码。CHANGELOG 记作「BoxService is now Sync」。第一个包含它的版本是 tower 0.5.0。util feature 从此多依赖一个 sync_wrapper(当时是 0.1.1)。

四种盒子

类型对内层的要求自身构造入口
UnsyncBoxServiceS: 'static,S::Future: 'static不 Send、不 Sync、不 Clone;future 不 SendUnsyncBoxService::new、::layer()
BoxServiceS: Send + 'static,S::Future: Send + 'staticSend + Sync(SyncWrapper),不 CloneServiceExt::boxed、ServiceBuilder::boxed、BoxService::new、::layer()
BoxCloneService再加 S: CloneSend + Clone,不 SyncServiceExt::boxed_clone、ServiceBuilder::boxed_clone、::new、::layer()
BoxCloneSyncService再加 S: SyncSend + Sync + CloneServiceBuilder::boxed_clone_sync、::new、::layer();ServiceExt 上没有对应方法

后三种的 future 都是 Pin<Box<dyn Future + Send>>(BoxCloneService 一族写成 futures_util 的 BoxFuture<'static, _>,是同一个类型)。

几处细节:

  • UnsyncBoxService 用于 future 不是 Send 的场合(比如 future 里持有 Rc),只能在单线程运行时上用。它没有走 map_future,而是包一层私有的 UnsyncBoxed,在 call 里 Box::pin。BoxService 以前也是这样写的,#598 把它换成了 map_future,「which is a bit shorter」,只改了 sync.rs。
  • BoxCloneService 能 Clone,靠的是一个私有 trait CloneService:Clone 本身不能做成 trait object(clone 返回 Self),于是加一个返回 Box<dyn CloneService<..> + Send> 的 clone_box,再给所有 Service + Send + Clone + 'static 的类型实现它。Clone for BoxCloneService 只是 Self(self.0.clone_box())。克隆走的是内层自己的 Clone,就绪状态带不带过去由内层决定,规则见 Service trait。
  • BoxCloneSyncService 是 0.5.2 加的(#777,来自 issue #770:「Currently BoxCloneService does not implement Sync even though the inner Service implements Sync.」)。boxed_clone_sync.rs 里的 CloneService 是同名的另一个私有 trait,只多了 + Sync。
  • ServiceBuilder::boxed_clone_sync 对 future 的要求是 Send + Sync + 'static,比 BoxCloneSyncService::new 的 Send + 'static 多一个 Sync。这个方法还会把之前整串 layer 一起擦掉,见 ServiceBuilder。
📝NOTE
没有蠢问题

问: 我 .boxed() 之后想把服务 clone 给每个连接,编译器说 BoxService 没有实现 Clone。为什么不干脆让它能克隆?

答: 克隆一个 trait object 需要内层能克隆,而 BoxService::new 不要求 S: Clone,很多服务(RateLimit、SpawnReady、Reconnect)本来就不能克隆。所以能克隆的版本单独做成了 BoxCloneService,用 .boxed_clone() 得到,前提是内层 Clone。内层不能 Clone 又要多处共享时,用 Buffer 把它放进后台 worker,Buffer 本身可以克隆。

BoxLayer 一族

服务能擦,layer 也能擦。BoxLayer 把任意 layer 包成一个类型固定的 layer,产出的服务是 BoxService:

pub struct BoxLayer<In, T, U, E> {
    boxed: Arc<dyn Layer<In, Service = BoxService<T, U, E>> + Send + Sync + 'static>,
}

impl<In, T, U, E> BoxLayer<In, T, U, E> {
    pub fn new<L>(inner_layer: L) -> Self
    where
        L: Layer<In> + Send + Sync + 'static,
        L::Service: Service<T, Response = U, Error = E> + Send + 'static,
        <L::Service as Service<T>>::Future: Send + 'static,
    {
        let layer = layer_fn(move |inner: In| {
            let out = inner_layer.layer(inner);
            BoxService::new(out)
        });

        Self {
            boxed: Arc::new(layer),
        }
    }
}
  • 用 layer_fn 把「先套原来的 layer,再 BoxService::new」做成一个新 layer,Arc 起来擦成 dyn Layer。
  • 用 Arc 而不是 Box,Clone 就只是加引用计数,不要求原来的 layer 能克隆。
  • 类型参数里的 In 是内层服务的类型,擦不掉:Layer<In> 的输入类型仍然要固定。

它的文档例子就是本页开头的问题换到 layer 上:只有设置了环境变量才加 timeout,两个分支的 layer 类型不同,各自 BoxLayer::new 之后就是同一个类型。另外两个是 BoxCloneServiceLayer(产出 BoxCloneService,要求 L::Service: Clone)和 BoxCloneSyncServiceLayer(产出 BoxCloneSyncService,再要求 Sync)。没有产出 UnsyncBoxService 的 layer 版本,dyn Layer 本身要 Send + Sync。

动脑筋答案

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

stack                        0 allocations / 1000 calls
stack.boxed()                1000 allocations / 1000 calls
stack.boxed_clone()          1000 allocations / 1000 calls
  • stack:0 次。三层组合子的 future 是嵌套的结构体,pin! 把它放在栈上,整个请求路径不碰堆。
  • boxed():每次 call 一次,就是 map_future 里那个 Box::pin(f)。BoxService 自己的那次 Box::new 发生在构造时,不在计数范围内。
  • boxed_clone():同样每次一次,BoxCloneService::new 也是 map_future(|f| Box::pin(f) as _)。

小结

  • 分支返回不同的中间件栈时报 E0308;照 rustc 的建议改成 Box<dyn Service> 又报 E0191,因为 Future 也得写出来。BoxService 把 future 装箱、把服务装进 trait object,两个问题一起解决。
  • BoxService::new = map_future(Box::pin) + Box::new + SyncWrapper::new;要求 S 和 S::Future 都 Send + 'static。
  • SyncWrapper 无条件 Sync,因为它不给 &self 访问;Service 的方法都是 &mut self,所以 BoxService 是 Sync,内层不必是(#702,0.5.0)。
  • BoxCloneService 能 Clone 但永远不是 Sync;要两者都有用 BoxCloneSyncService,代价是内层必须 Sync。UnsyncBoxService 给 future 不 Send 的场合用。
  • 代价:每个请求一次堆分配,poll_ready、call 和 poll future 都变成动态分发。BoxLayer 一族用 Arc<dyn Layer> 擦 layer 的类型。

💬 关联讨论

发起讨论
加载中...