Rust in Everything
基于 tower 0.5.3

从零写一个 tower

主线位置:两条主线都自己实现一遍。ConcurrencyLimit::poll_ready 是就绪主线上的一站:取不到许可就 Pending;Timeout 和 ConcurrencyLimit 的 future 是请求主线上的两层包装。

flowchart TB
  b["Builder::new()"] -->|"layer(TimeoutLayer)"| b1["Builder Stack"]
  b1 -->|"layer(ConcurrencyLimitLayer)"| b2["Builder Stack Stack"]
  b2 -->|"service(mock)"| svc["Timeout ConcurrencyLimit Mock"]
  svc --> pr[poll_ready 往里传]
  svc --> callreq[call 往里调]
  callreq --> fut["TimeoutFuture 包 LimitFuture"]
  fut --> mk[请求到达 mock]
  mk --> h[测试代码收到请求]
  h -->|决定何时回复| fut

本页只讲思路和关键代码。下面的片段都摘自同一个文件,它只依赖 tokio 和 pin-project-lite,能编译、能跑:

tokio = { version = "=1.54.1", features = ["rt", "macros", "sync", "time"] }
pin-project-lite = "0.2"

完整实现、Buffer 与 P2C 负载均衡的从零实现,以及配套作业,放在付费课程里。

两个 trait

type BoxError = Box<dyn Error + Send + Sync>;

trait Service<Req> {
  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>>;
  fn call(&mut self, req: Req) -> Self::Future;
}

trait Layer<S> {
  type Service;
  fn layer(&self, inner: S) -> Self::Service;
}

和 tower 的 Service、Layer 的方法签名相同(tower 里另有文档和几个转发实现,这里不需要)。设计上的理由在 Service trait 和 Layer 与 Stack 里讲过,这里只提醒三点:poll_ready 收 &mut self,所以它能把占到的资源存在服务身上;call 也收 &mut self,返回的 future 不借用服务,能活得比这次借用久;Future 是关联类型,每一层都能给出自己的具体类型,不必装箱。

叠起来:Stack 和 Builder

struct Identity;

impl<S> Layer<S> for Identity {
  type Service = S;
  fn layer(&self, inner: S) -> S {
    inner
  }
}

struct Stack<Inner, Outer> {
  inner: Inner,
  outer: Outer,
}

impl<S, Inner: Layer<S>, Outer: Layer<Inner::Service>> Layer<S> for Stack<Inner, Outer> {
  type Service = Outer::Service;
  fn layer(&self, service: S) -> Self::Service {
    self.outer.layer(self.inner.layer(service))
  }
}

struct Builder<L>(L);

impl Builder<Identity> {
  fn new() -> Self {
    Builder(Identity)
  }
}

impl<L> Builder<L> {
  // 新加的 layer 放在 Stack 的 inner 一侧:先加的在外层
  fn layer<T>(self, layer: T) -> Builder<Stack<T, L>> {
    Builder(Stack { inner: layer, outer: self.0 })
  }
  fn service<S>(&self, service: S) -> L::Service
  where
    L: Layer<S>,
  {
    self.0.layer(service)
  }
}

和 tower 的 Stack::layer、ServiceBuilder::layer 是同一个做法:新的 layer 进 inner,已有的整串进 outer。service 时先套 inner(最后加的),所以先加的在最外层。Builder::new().layer(TimeoutLayer(..)).layer(ConcurrencyLimitLayer(1)).service(mock) 的类型名打出来是 Timeout<ConcurrencyLimit<Mock>>。

Timeout:只动请求主线

impl<S, Req> Service<Req> for Timeout<S>
where
  S: Service<Req>,
  S::Error: Into<BoxError>,
{
  type Response = S::Response;
  type Error = BoxError;
  type Future = TimeoutFuture<S::Future>;

  fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), BoxError>> {
    // 就绪主线:原样转发,等待就绪的时间不计入超时
    self.inner.poll_ready(cx).map_err(Into::into)
  }

  fn call(&mut self, req: Req) -> Self::Future {
    // 请求主线:从 call 开始计时
    TimeoutFuture { inner: self.inner.call(req), sleep: tokio::time::sleep(self.timeout) }
  }
}

pin_project! {
  struct TimeoutFuture<F> {
    #[pin]
    inner: F,
    #[pin]
    sleep: tokio::time::Sleep,
  }
}

impl<F, T, E> Future for TimeoutFuture<F>
where
  F: Future<Output = Result<T, E>>,
  E: Into<BoxError>,
{
  type Output = Result<T, BoxError>;

  fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
    let this = self.project();
    // 先 poll 内层:同一轮里两边都好了,算成功
    if let Poll::Ready(res) = this.inner.poll(cx) {
      return Poll::Ready(res.map_err(Into::into));
    }
    ready!(this.sleep.poll(cx));
    Poll::Ready(Err(Elapsed.into()))
  }
}

(Elapsed 是一个实现了 Error 的空结构体,TimeoutLayer 只存一个 Duration,从略。)

要写对的地方有三处:

  • 错误类型统一成 BoxError。 超时是 Timeout 自己产生的错误,内层又有自己的错误,两者只能合进一个能装下任何错误的类型。tower 的 Timeout 也是这样。
  • Sleep 不是 Unpin,所以 future 里要用 pin_project! 把它和内层 future 都标成 #[pin],poll 里通过投影拿到 Pin<&mut _>。
  • 先 poll 内层,再 poll Sleep。 和 tower 的 ResponseFuture::poll 顺序相同:两边同时就绪时,响应优先。

ConcurrencyLimit:两条主线都动

type Acquire = Pin<Box<dyn Future<Output = Result<OwnedSemaphorePermit, AcquireError>> + Send>>;

struct ConcurrencyLimit<S> {
  inner: S,
  semaphore: Arc<Semaphore>,
  acquiring: Option<Acquire>,
  permit: Option<OwnedSemaphorePermit>,
}

impl<S, Req> Service<Req> for ConcurrencyLimit<S>
where
  S: Service<Req>,
{
  type Response = S::Response;
  type Error = S::Error;
  type Future = LimitFuture<S::Future>;

  fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), S::Error>> {
    if self.permit.is_none() {
      let sem = &self.semaphore;
      let acquiring = self.acquiring.get_or_insert_with(|| Box::pin(sem.clone().acquire_owned()));
      // 取不到许可就 Pending;acquire_owned 的 future 记下了 waker
      let permit = ready!(acquiring.as_mut().poll(cx)).expect("semaphore is never closed");
      self.acquiring = None;
      self.permit = Some(permit);
    }
    self.inner.poll_ready(cx)
  }

  fn call(&mut self, req: Req) -> Self::Future {
    let permit = self.permit.take().expect("poll_ready must be called first");
    LimitFuture { inner: self.inner.call(req), _permit: permit }
  }
}

pin_project! {
  struct LimitFuture<F> {
    #[pin]
    inner: F,
    _permit: OwnedSemaphorePermit, // future 被 drop 时归还
  }
}

(ConcurrencyLimitLayer 新建一个 Arc<Semaphore>;手写的 Clone 共享信号量、不带 acquiring 和 permit;LimitFuture::poll 只是转发。都从略。)

就绪主线上的关键是 acquiring 这个字段。tokio 的 Semaphore 只提供 async fn acquire_owned,没有能在 poll_ready 里直接调的 poll_acquire。所以第一次取不到许可时,把 acquire_owned() 的 future 装箱存起来,下次 poll_ready 接着 poll 同一个 future:它在信号量的等待队列里的位置留着,waker 也登记在它身上。如果每次 poll_ready 都新建一个,就会反复排到队尾。

这个写法每开始一次取许可就分配一个 Box。tower 0.4 早期就是这么做的,后来改用 tokio-util 的 PollSemaphore 复用同一块分配,那段来历见 ConcurrencyLimit 的冷知识。

请求主线上的关键是 LimitFuture 带着许可:许可的寿命等于响应 future 的寿命,完成、超时、取消都会在 drop 时归还,不需要写任何「归还」的代码。

手写一个 mock

要看到背压,需要一个「什么时候回响应由测试代码决定」的内层服务。tower 有现成的 tower-test,这里用两条 tokio 通道自己写一个:

type Responder = oneshot::Sender<Result<String, BoxError>>;

#[derive(Clone)]
struct Mock {
  tx: mpsc::UnboundedSender<(u32, Responder)>,
}

struct Handle {
  rx: mpsc::UnboundedReceiver<(u32, Responder)>,
}

fn mock() -> (Mock, Handle) {
  let (tx, rx) = mpsc::unbounded_channel();
  (Mock { tx }, Handle { rx })
}

impl Service<u32> for Mock {
  type Response = String;
  type Error = BoxError;
  type Future = Pin<Box<dyn Future<Output = Result<String, BoxError>> + Send>>;

  fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), BoxError>> {
    Poll::Ready(Ok(()))
  }

  fn call(&mut self, req: u32) -> Self::Future {
    // 请求交给测试代码,响应由测试代码决定什么时候给
    let (resp_tx, resp_rx) = oneshot::channel();
    let _ = self.tx.send((req, resp_tx));
    Box::pin(async move { resp_rx.await.map_err(|_| BoxError::from("mock dropped"))? })
  }
}

impl Handle {
  fn try_next(&mut self) -> Option<(u32, Responder)> {
    self.rx.try_recv().ok()
  }
}

每次 call,mock 把请求和一个 oneshot::Sender 一起送进 mpsc,自己返回等 oneshot 的 future。测试代码从 Handle 取出请求,什么时候 send 回复,响应 future 就什么时候完成。tower-test 的 pair 是同一个思路,另外还能用 allow 控制 mock 自己的 poll_ready;这里用不上,背压来自 ConcurrencyLimit。

验证背压

poll_once 用一个自制的 waker 调一次 poll_ready,waker 被叫醒时把一个 AtomicBool 置成 true;ready 用 poll_fn 把 poll_ready 包成可以 .await 的 future(两个辅助函数从略)。

async fn run<S>(mut svc: S, mut handle: Handle)
where
  S: Service<u32, Response = String, Error = BoxError>,
{
  let flag = Arc::new(Flag(AtomicBool::new(false)));
  println!("1 poll_ready: {}", poll_once(&mut svc, &flag));
  let first = svc.call(1);
  println!("2 poll_ready: {}", poll_once(&mut svc, &flag));

  let (req, responder) = handle.try_next().expect("first request reached the mock");
  println!("3 mock got {req}, more queued: {}", handle.try_next().is_some());

  responder.send(Ok(format!("resp {req}"))).unwrap();
  let first = first.await.map_err(|e| e.to_string());
  println!("4 first: {first:?}, waker woken: {}", flag.0.load(Ordering::SeqCst));
  println!("5 poll_ready: {}", poll_once(&mut svc, &flag));

  let second = svc.call(2);
  let (_req, _responder) = handle.try_next().expect("second request reached the mock");
  println!("6 second: {:?}", second.await.map_err(|e| e.to_string()));
  ready(&mut svc).await.unwrap();
  println!("7 ready again after timeout");
}

#[tokio::main(flavor = "current_thread")]
async fn main() {
  println!("Timeout outside ConcurrencyLimit:");
  let (inner, handle) = mock();
  let svc = Builder::new()
    .layer(TimeoutLayer(Duration::from_millis(50)))
    .layer(ConcurrencyLimitLayer(1))
    .service(inner);
  run(svc, handle).await;

  println!("ConcurrencyLimit outside Timeout:");
  let (inner, handle) = mock();
  let svc = Builder::new()
    .layer(ConcurrencyLimitLayer(1))
    .layer(TimeoutLayer(Duration::from_millis(50)))
    .service(inner);
  run(svc, handle).await;
}

第一组(Timeout 在外)的输出:

Timeout outside ConcurrencyLimit:
1 poll_ready: Ready(Ok)
2 poll_ready: Pending
3 mock got 1, more queued: false
4 first: Ok("resp 1"), waker woken: true
5 poll_ready: Ready(Ok)
6 second: Err("request timed out")
7 ready again after timeout

逐行对应:

  1. 上限是 1,取到许可,存在 ConcurrencyLimit.permit 里。
  2. 许可已经被 call 移进 first 的 LimitFuture。再问,acquire_owned 的 future 排进等待队列,返回 Pending,Timeout 原样往外传。这就是背压:调用方根本没机会发第二个请求。
  3. mock 只收到一个请求,第二个请求被挡在了 poll_ready 之外。
  4. 测试代码回复,first 完成并被 drop,许可归还。信号量把它直接交给排着队的那个 acquire_owned future,并叫醒它登记的 waker,也就是第 2 步的 flag。
  5. 许可已经在等着它了,Ready。
  6. mock 收到请求 2 却不回复,50ms 后 TimeoutFuture 的 Sleep 先到,返回超时错误。
  7. 超时的 future 被 drop,里面的 LimitFuture 跟着 drop,许可归还,所以又能 ready。没有任何代码专门处理「超时后归还许可」。
❗IMPORTANT
动脑筋:把两层的顺序对调(ConcurrencyLimit 在外、Timeout 在内),7 行输出会有哪一行变?

第二组就是对调后的版本,run 一字没改。先想想第 2 行还是不是 Pending,第 7 行还能不能 ready,再看文末。

动脑筋答案

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

Timeout outside ConcurrencyLimit:
1 poll_ready: Ready(Ok)
2 poll_ready: Pending
3 mock got 1, more queued: false
4 first: Ok("resp 1"), waker woken: true
5 poll_ready: Ready(Ok)
6 second: Err("request timed out")
7 ready again after timeout
ConcurrencyLimit outside Timeout:
1 poll_ready: Ready(Ok)
2 poll_ready: Pending
3 mock got 1, more queued: false
4 first: Ok("resp 1"), waker woken: true
5 poll_ready: Ready(Ok)
6 second: Err("request timed out")
7 ready again after timeout

一行都没变。

  • 第 2 行:Timeout::poll_ready 只是转发,取许可只发生在 ConcurrencyLimit 里,它在外在内都一样会 Pending。
  • 第 6 行:对调后 LimitFuture 在外,TimeoutFuture 在内。超时错误从内层冒出来,LimitFuture::poll 原样转发,结果相同。
  • 第 7 行:两种顺序下许可都在这个请求的 future 树里,整棵树被 drop 时许可归还。区别只在许可挂在哪一层,不影响归还。

这两层能随便换,是因为 Timeout 不碰就绪主线。换成会在 poll_ready 里等待的中间件(比如 Buffer),Timeout 在它里面还是外面,决定了排队时间算不算进超时,见 Timeout。

小结

  • Service 和 Layer 两个 trait 加起来十几行;Stack<Inner, Outer> 把新 layer 放进 inner,所以 Builder 先加的在外层。
  • Timeout:poll_ready 转发,call 时建 Sleep,future 先 poll 内层再 poll Sleep,错误统一成 BoxError。
  • ConcurrencyLimit:poll_ready 里把 acquire_owned 的 future 装箱存住,保住排队位置;call 把许可移进响应 future,drop 时归还。
  • 用 mpsc 加 oneshot 写的 mock 让测试代码控制响应时机,能直接看到 poll_ready 返回 Pending、许可归还时 waker 被叫醒。
  • Timeout 和 ConcurrencyLimit 对调顺序,行为不变:Timeout 不碰就绪主线。

💬 关联讨论

发起讨论
加载中...