Rust in Everything
基于 tower 0.5.3

ServiceExt 与 Oneshot

主线位置:调用方这一端。ready 是就绪主线的入口;oneshot 一次走完两条主线;call_all 把两条主线排成流水线,一边等就绪一边收响应。

stateDiagram-v2
  [*] --> NotReady: Oneshot new
  NotReady --> NotReady: poll_ready Pending
  NotReady --> Called: Ready Ok 后 call
  NotReady --> [*]: Ready Err 返回错误
  Called --> Called: future Pending
  Called --> Done: future Ok
  Called --> [*]: future Err 返回错误
  Done --> [*]: 返回响应

Service 规定了契约,但手写 poll_fn(|cx| svc.poll_ready(cx)).await 再 call 很啰嗦。ServiceExt 把常用的调用方式包成 future 和 stream。它在 util/mod.rs,要开 util feature,对所有服务自动实现(第 1045 行):

impl<T: ?Sized, Request> ServiceExt<Request> for T where T: tower_service::Service<Request> {}

本页讲其中和调用有关的四个方法:ready、ready_oneshot、oneshot、call_all。map_request、boxed 这些组合子放在第 03 部分。

ready 与 ready_oneshot

两个方法都只做一件事:反复 poll_ready,直到 Ready(Ok)。ReadyOneshot 是实现,Ready 是它的借用版:

pub struct ReadyOneshot<T, Request> {
    inner: Option<T>,
    _p: PhantomData<fn() -> Request>,
}

// Safety: This is safe because `Services`'s are always `Unpin`.
impl<T, Request> Unpin for ReadyOneshot<T, Request> {}

impl<T, Request> Future for ReadyOneshot<T, Request>
where
    T: Service<Request>,
{
    type Output = Result<T, T::Error>;

    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        ready!(self
            .inner
            .as_mut()
            .expect("poll after Poll::Ready")
            .poll_ready(cx))?;

        Poll::Ready(Ok(self.inner.take().expect("poll after Poll::Ready")))
    }
}

pub struct Ready<'a, T, Request>(ReadyOneshot<&'a mut T, Request>);
方法拿什么得到什么典型用法
ready&mut selfResult<&mut Self, Error>svc.ready().await?.call(req).await
ready_oneshotselfResult<Self, Error>等就绪之后把服务整个移走,比如移进一个新任务

几处细节:

  • Ready 里装的是 ReadyOneshot<&'a mut T, Request>:这里的服务是 &mut T,能这样写是因为 &mut S 也实现了 Service(见 Service trait 的转发实现)。
  • 手写的 Unpin:poll_ready 拿的是 &mut self,服务从来不需要 pin,所以不管 T 是不是 Unpin,这个 future 都可以是 Unpin。注释写着「Safety」,但实现 Unpin 本身不需要 unsafe,tower 整个 crate 都是 forbid(unsafe_code)。
  • poll_ready 返回 Err 时,? 直接把错误返回,服务还留在 inner 里,随这个 future 一起 drop。对 ready_oneshot 来说,服务报废后调用方拿不回它,正好符合「Ready(Err) 之后丢弃实例」的规则。

ready().await 只保证这一刻就绪。按规则 2,在 call 之前服务要一直保持就绪,所以 ready 和 call 之间可以有别的 .await,但不要拿一个克隆去 call。

📝NOTE
冷知识:ready 曾经不返回服务

tower-util 0.3 时代,ServiceExt::ready 的 future 输出是 Result<(), Error>,没法接着写 .call(req)。2020 年 3 月的 #427 加了 ready_and,说明里写:「ServiceExt::ready says that it produces "A future yielding the service when it is ready to accept a request." This is not true; it does not yield the service when it is ready, it yields unit. This makes it impossible to chain service ready with service call」。当时不能直接改 ready 的返回类型,因为那是破坏性变更,于是先加 ready_and、把 ready 标成 deprecated(随 tower-util 0.3.1 发布)。

之后分三步收尾:#507 在 tower 0.4.0 删掉旧的 ready;#567 在 0.4.6 把 ready_and 改名为 ready、ReadyAnd 改名为 Ready,旧名字 deprecated;#652 在 0.5.0 删掉 ready_and。所以今天的 Ready<'a, T, Request> 就是当年的 ReadyAnd。

Oneshot:NotReady → Called → Done

oneshot(req) 拿走服务和一个请求,返回 Oneshot。它是一个三态的状态机,State 和 poll:

pin_project! {
    #[project = StateProj]
    enum State<S: Service<Req>, Req> {
        NotReady {
            svc: S,
            req: Option<Req>,
        },
        Called {
            #[pin]
            fut: S::Future,
        },
        Done,
    }
}

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        let mut this = self.project();
        loop {
            match this.state.as_mut().project() {
                StateProj::NotReady { svc, req } => {
                    ready!(svc.poll_ready(cx))?;
                    let f = svc.call(req.take().expect("already called"));
                    this.state.set(State::called(f));
                }
                StateProj::Called { fut } => {
                    let res = ready!(fut.poll(cx))?;
                    this.state.set(State::Done);
                    return Poll::Ready(Ok(res));
                }
                StateProj::Done => panic!("polled after complete"),
            }
        }
    }
  • NotReady:poll_ready 返回 Pending 就原样返回,服务和请求都留在状态里,下次再 poll。就绪以后 call,然后 this.state.set(State::called(f)):整个 NotReady 变体被覆盖,服务在这一刻被 drop,此后只剩它返回的 future。
  • Called:fut 被 pin 在状态里(#[pin]),所以 Oneshot 本身不是无条件 Unpin。loop 让 call 之后立刻接着 poll 一次 future,不用多等一轮唤醒。
  • Done:只在成功路径上进入,再 poll 就 panic polled after complete。

注意两个 ?:poll_ready 报错时状态留在 NotReady,future 报错时状态留在 Called,都直接返回 Err,不会进 Done。按 Future 的约定,调用方本来就不该在拿到结果之后再 poll,所以这不影响正常使用;但如果有人这么做了,panic 的不是 Oneshot 自己的那句话。

用一个会先 Pending 两次、并在 drop 时打印的服务看状态转换:

tower = { version = "=0.5.3", features = ["util"] }
tokio = { version = "=1.54.1", features = ["full"] }
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use tower::{Service, ServiceExt};

struct Svc {
  polls: u32,
}

impl Service<u32> for Svc {
  type Response = u32;
  type Error = &'static str;
  type Future = Pin<Box<dyn Future<Output = Result<u32, &'static str>>>>;

  fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
    self.polls += 1;
    if self.polls < 3 {
      println!("poll_ready #{} -> Pending", self.polls);
      cx.waker().wake_by_ref();
      Poll::Pending
    } else {
      println!("poll_ready #{} -> Ready", self.polls);
      Poll::Ready(Ok(()))
    }
  }

  fn call(&mut self, req: u32) -> Self::Future {
    println!("call({req})");
    Box::pin(async move {
      tokio::task::yield_now().await;
      println!("response future done");
      Ok(req + 1)
    })
  }
}

impl Drop for Svc {
  fn drop(&mut self) {
    println!("Svc dropped");
  }
}

#[tokio::main(flavor = "current_thread")]
async fn main() {
  let rsp = Svc { polls: 0 }.oneshot(41).await;
  println!("oneshot -> {rsp:?}");
}

M2 Max、rustc 1.99.0、--release:

poll_ready #1 -> Pending
poll_ready #2 -> Pending
poll_ready #3 -> Ready
call(41)
Svc dropped
response future done
oneshot -> Ok(42)

Svc dropped 出现在 call(41) 之后、响应完成之前。对 ConcurrencyLimit 这类服务没有影响,因为许可已经随 call 移进了 future;但如果某个服务把「请求还在进行」的状态放在服务自己身上,用 oneshot 调它时这个状态会提前消失。

失败路径:服务的 future 直接是 std::future::ready(Err("boom")),用 tokio_test::task::spawn 手动 poll 两次(依赖再加 tokio-test = "0.4"):

use std::future::{ready, Ready};
use std::task::{Context, Poll};
use tokio_test::task;
use tower::{Service, ServiceExt};

#[derive(Debug)]
struct Fails;

impl Service<u32> for Fails {
  type Response = u32;
  type Error = &'static str;
  type Future = Ready<Result<u32, &'static str>>;
  fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
    Poll::Ready(Ok(()))
  }
  fn call(&mut self, _req: u32) -> Self::Future {
    ready(Err("boom"))
  }
}

fn main() {
  let mut fut = task::spawn(Fails.oneshot(1));
  println!("first poll: {:?}", fut.poll());
  println!("{:?}", fut.enter(|_, f| format!("{:?}", f)));
  println!("second poll: {:?}", fut.poll());
}
first poll: Ready(Err("boom"))
"Oneshot { state: State::Called(\"S::Future\") }"

thread 'main' (16814438) panicked at ~/.rustup/toolchains/stable-aarch64-apple-darwin/lib/rustlib/src/rust/library/core/src/future/ready.rs:23:35:
`Ready` polled after completion
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

返回 Err 之后状态仍是 Called(第二行是 Oneshot 的 Debug 输出),第二次 poll 落到了已经完成的内层 future 上,panic 来自标准库的 Ready。

📝NOTE
冷知识:req 为什么单独放在 Option 里

NotReady { svc, req: Option<Req> } 里只有请求是 Option,这是修一个 bug 修出来的。#447 的说明:把 Oneshot 从 futures 0.1 移植到 std::future 时(#330),「an Option is used to store the request while waiting for the service to become ready ... However, the Option contains both the request and the service itself, and is taken the first time the service is polled」。服务没就绪时 ready! 提前返回,取出来的服务和请求就被 drop 了,「if the oneshot future is polled again, it will panic」。

修法:「only the request lives in the Option, and it is only taken when the service is called, rather than every time it is polled.」也就是上面 req.take().expect("already called") 只在 call 那一行出现。第一个包含修复的 tower 版本是 0.4.0。上面实验里前两次 Pending 之后服务还活着,靠的就是这一改。

call_all:请求流变响应流

call_all(reqs) 拿一个服务和一个请求 Stream,返回响应的 Stream。有序版 CallAll 和无序版 CallAllUnordered 共用 common::CallAll,区别只在存放在途 future 的队列 Q:

队列push响应顺序
CallAllFuturesOrdered<Svc::Future>push_back(第 165 行)和请求顺序相同
CallAllUnorderedFuturesUnordered<Svc::Future>push(第 86 行)谁先完成谁先出

两种队列都实现了 crate 内部的 Drive trait(is_empty、push、poll),通用逻辑只写一遍。无序版通过 CallAll::unordered() 得到,它要求还没开始用:assert!(self.queue.is_empty() && !self.eof)。

核心是 poll_next:

        loop {
            // First, see if we have any responses to yield
            if let Poll::Ready(r) = this.queue.poll(cx) {
                if let Some(rsp) = r.transpose()? {
                    return Poll::Ready(Some(Ok(rsp)));
                }
            }

            // If there are no more requests coming, check if we're done
            if *this.eof {
                if this.queue.is_empty() {
                    return Poll::Ready(None);
                } else {
                    return Poll::Pending;
                }
            }

            // If not done, and we don't have a stored request, gather the next request from the
            // stream (if there is one), or return `Pending` if the stream is not ready.
            if this.curr_req.is_none() {
                *this.curr_req = match ready!(this.stream.as_mut().poll_next(cx)) {
                    Some(next_req) => Some(next_req),
                    None => {
                        // Mark that there will be no more requests.
                        *this.eof = true;
                        continue;
                    }
                };
            }

            // Then, see that the service is ready for another request
            let svc = this
                .service
                .as_mut()
                .expect("Using CallAll after extracting inner Service");

            if let Err(e) = ready!(svc.poll_ready(cx)) {
                // Set eof to prevent the service from being called again after a `poll_ready` error
                *this.eof = true;
                return Poll::Ready(Some(Err(e)));
            }

            // Unwrap: The check above always sets `this.curr_req` if none.
            this.queue.push(svc.call(this.curr_req.take().unwrap()));
        }
flowchart TB
  q{队列里有完成的?} -->|有| out[吐出响应或错误]
  q -->|没有| eof{eof?}
  eof -->|是| fin["队列空则结束,否则 Pending"]
  eof -->|否| req["取下一个请求到 curr_req"]
  req --> pr{poll_ready}
  pr -->|Pending| pend[返回 Pending]
  pr -->|Err| stop["eof = true,吐出错误"]
  pr -->|Ok| push["call,future 进队列"]
  push --> q

读这段循环要注意四点:

  1. 先取请求,再 poll_ready。 请求流 Pending 时还没碰服务,不会出现「占了资源却迟迟等不来请求」。poll_ready 一旦 Ready(Ok),下一行立刻 call。
  2. 并发度由服务决定。 只要服务一直就绪,循环就一直取请求、一直 call,在途 future 全在队列里。CallAll 自己不限并发,背压完全来自服务的 poll_ready。
  3. 响应错误不结束流。 队列里某个 future 返回 Err,transpose()? 把它作为一项吐出,下一次 poll_next 照常继续。
  4. poll_ready 出错就结束。 设 eof = true,吐出这个错误,之后不再取请求、不再 call,只把已经在队列里的 future 跑完。已经取出来放在 curr_req 里的那个请求不会被发出去。
📝NOTE
冷知识:call_all 的两个 bug 一起修的

issue #706 指出:文档说 poll_ready 报错后服务应当丢弃,「But CallAll will currently treat a poll_ready error in the same way as a request error and continues to call the service.」

#709 同时修了两件事。一是上面第 4 点:poll_ready 出错时设 eof。二是第 1 点的顺序:原来的代码先 ready!(svc.poll_ready(cx))?,再去 poll 请求流,旁边留着一条 TODO:「We probably want to "release" the slot we reserved in Svc if the stream returns Pending. It may be a while until we get around to actually using it.」修复的做法是加 curr_req 字段,先拿到请求再问服务,PR 的说法是「since there is no disarm this makes sure poll_ready is only called if call will immediately follow. This one actually hit me in production.」

第一个包含这两处修复的 tower 版本是 0.5.0。

先看有序和无序的差别。请求 n 要处理 n × 10ms,时钟暂停(start_paused,需要 tokio 的 test-util feature),结果是确定的:

tower = { version = "=0.5.3", features = ["util"] }
tokio = { version = "=1.54.1", features = ["full", "test-util"] }
futures = "0.3"
use futures::stream::{self, StreamExt};
use std::convert::Infallible;
use std::time::Duration;
use tower::{service_fn, ServiceExt};

#[tokio::main(flavor = "current_thread", start_paused = true)]
async fn main() {
  // request n takes n * 10ms
  let svc = service_fn(|n: u64| async move {
    tokio::time::sleep(Duration::from_millis(n * 10)).await;
    Ok::<_, Infallible>(n)
  });
  let reqs = [3u64, 1, 2];

  let ordered: Vec<u64> = svc
    .call_all(stream::iter(reqs))
    .map(Result::unwrap)
    .collect()
    .await;
  println!("call_all:             {ordered:?}");

  let unordered: Vec<u64> = svc
    .call_all(stream::iter(reqs))
    .unordered()
    .map(Result::unwrap)
    .collect()
    .await;
  println!("call_all().unordered: {unordered:?}");
}

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

call_all:             [3, 1, 2]
call_all().unordered: [1, 2, 3]

有序版按请求顺序 [3, 1, 2] 吐出,即使 1 最先完成也要等 3;无序版按完成顺序。service_fn 永远就绪,所以三个请求在第一次 poll_next 里就全部发出去了。

❗IMPORTANT
动脑筋:poll_ready 第三次报错

下面的服务第 3 次 poll_ready 返回 Err,其余都就绪;每个请求等 10ms,请求 2 的响应是 Err。用 &mut svc 调 call_all,跑完后还能读计数。依赖同上一个实验。

use futures::stream::{self, StreamExt};
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use std::time::Duration;
use tower::{Service, ServiceExt};

#[derive(Default)]
struct Flaky {
  polls: u32,
  calls: u32,
}

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

  fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), String>> {
    self.polls += 1;
    if self.polls == 3 {
      Poll::Ready(Err("poll_ready failed".to_string()))
    } else {
      Poll::Ready(Ok(()))
    }
  }

  fn call(&mut self, req: u32) -> Self::Future {
    self.calls += 1;
    Box::pin(async move {
      tokio::time::sleep(Duration::from_millis(10)).await;
      if req == 2 {
        Err(format!("request {req} failed"))
      } else {
        Ok(req)
      }
    })
  }
}

#[tokio::main(flavor = "current_thread", start_paused = true)]
async fn main() {
  let mut svc = Flaky::default();
  let mut rsps = (&mut svc).call_all(stream::iter(1..=5));
  while let Some(item) = rsps.next().await {
    println!("{item:?}");
  }
  drop(rsps);
  println!("poll_ready = {}, call = {}", svc.polls, svc.calls);
}
响应流吐出几项、按什么顺序?最后一行的两个数是多少?

动脑筋答案

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

Err("poll_ready failed")
Ok(1)
Err("request 2 failed")
poll_ready = 3, call = 2
  • 第一项就是 poll_ready 的错误。 第一次 poll_next 里,队列为空,取请求 1、就绪、call;取请求 2、就绪、call;取请求 3,poll_ready 报错,设 eof 并立刻返回。这时请求 1、2 的 future 还在睡,所以错误排在它们前面。
  • 之后照常吐出在途的响应。 eof 为真但队列不空,循环只 poll 队列:Ok(1),然后是 Err("request 2 failed")。这是响应错误,不结束流。队列空了才返回 None。
  • poll_ready = 3, call = 2。 请求 3 已经被取进 curr_req,但没有被 call,随 rsps 一起 drop;请求 4、5 根本没被从流里取出来。

小结

  • ServiceExt 对所有 Service 自动实现,要开 util feature。
  • ready 借用服务、ready_oneshot 拿走服务,都是反复 poll_ready 到 Ready(Ok);Ready 内部就是 ReadyOneshot<&mut T, _>。poll_ready 报错时服务随 future 丢弃。
  • Oneshot 是 NotReady → Called → Done 三态状态机:call 之后服务立刻被 drop,只留 future;只有成功路径进入 Done。
  • call_all 先取请求再 poll_ready,就绪就立刻 call;并发度由服务的就绪决定。有序版用 FuturesOrdered,.unordered() 换成 FuturesUnordered。
  • 响应错误不结束 call_all 的流;poll_ready 错误会:吐出错误、不再发请求、跑完已在途的响应后结束。

💬 关联讨论

发起讨论
加载中...