从零写一个 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,取到许可,存在
ConcurrencyLimit.permit里。 - 许可已经被
call移进first的LimitFuture。再问,acquire_owned的 future 排进等待队列,返回Pending,Timeout原样往外传。这就是背压:调用方根本没机会发第二个请求。 - mock 只收到一个请求,第二个请求被挡在了
poll_ready之外。 - 测试代码回复,
first完成并被 drop,许可归还。信号量把它直接交给排着队的那个acquire_ownedfuture,并叫醒它登记的 waker,也就是第 2 步的flag。 - 许可已经在等着它了,
Ready。 - mock 收到请求 2 却不回复,50ms 后
TimeoutFuture的Sleep先到,返回超时错误。 - 超时的 future 被 drop,里面的
LimitFuture跟着 drop,许可归还,所以又能ready。没有任何代码专门处理「超时后归还许可」。
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 内层再 pollSleep,错误统一成BoxError。ConcurrencyLimit:poll_ready里把acquire_owned的 future 装箱存住,保住排队位置;call把许可移进响应 future,drop 时归还。- 用
mpsc加oneshot写的 mock 让测试代码控制响应时机,能直接看到poll_ready返回Pending、许可归还时 waker 被叫醒。 Timeout和ConcurrencyLimit对调顺序,行为不变:Timeout不碰就绪主线。