Rust in Everything
基于 tower 0.5.3

Filter

主线位置:请求主线。poll_ready 原样转发;Filter 在 call 里检查请求,AsyncFilter 把检查放进响应 future。

flowchart TB
  call["call(req)"] --> kind{哪一种}
  kind -->|Filter| sc["predicate.check(req)"]
  sc -->|"Ok(req2)"| ic["inner.call(req2)"]
  sc -->|"Err(e)"| rej["立即完成的 Err"]
  kind -->|AsyncFilter| mv[换出已就绪的内层]
  mv --> fut["future:Check 状态"]
  fut -->|"Ok(req2)"| wc["inner.call(req2)"]
  wc --> wr[WaitResponse 状态]
  fut -->|"Err(e)"| rej2[返回 Err]

Filter 回答一个问题:这个请求要不要交给内层。检查函数叫谓词(predicate),它拿走请求,返回 Ok(新请求) 或 Err(错误)。新请求可以就是原来那个,也可以改过,甚至换了类型。

两个谓词 trait

AsyncPredicate 和 Predicate:

pub trait AsyncPredicate<Request> {
    // ...
    type Future: Future<Output = Result<Self::Request, BoxError>>;
    // ...
    type Request;
    // ...
    fn check(&mut self, request: Request) -> Self::Future;
}
/// Checks a request synchronously.
pub trait Predicate<Request> {
    // ...
    type Request;
    // ...
    fn check(&mut self, request: Request) -> Result<Self::Request, BoxError>;
}

两点:

  • 关联类型 Request 是检查之后的请求类型。Filter<T, U> 实现 Service<Request>,要求内层 T: Service<U::Request>,所以谓词可以把 String 解析成 u32 再交给只收 u32 的内层。
  • check 拿走请求、按值返回。检查和改写是同一步,不需要先借用检查、再复制一份去改。

两个 trait 都给闭包写了通用实现(Predicate 的、AsyncPredicate 的):FnMut(T) -> Result<R, E> 是同步谓词,FnMut(T) -> impl Future<Output = Result<R, E>> 是异步谓词,E: Into<BoxError> 就行,错误会被装箱。需要带状态的谓词(比如查一个 HashSet)就给自己的类型实现 trait。模块文档的建议是:用不着 .await 就用同步的,开销更小。

📝NOTE
冷知识:0.4 之前,filter 只能是异步的,而且不能改请求

#508(0.4.0)之前,tower::filter 的谓词是异步地检查 &Request,返回 Result<(), Error>,只能放行或拒绝。另一个组合子 try_with 则是同步地把 Request 映射成 Result<DifferentRequest, Error>。PR 说明指出两者有重叠,而且「It definitely seems like there's demand for both synchronous and asynchronous request filtering. However, the APIs we have currently differ pretty significantly.」它还提到 linkerd2-proxy 自己写了一个介于两者之间的 RequestFilter:同步、能改请求类型、用 trait 而不是闭包表示谓词。

#508 改成了现在的样子:「Predicates are synchronous by default. A separate AsyncFilter type and an AsyncPredicate trait are available for predicates returning futures.」「Predicates may now return a new Request type, allowing Filter and AsyncFilter to subsume try_map_request.」错误也从固定类型变成了任意能转成 BoxError 的类型。

Filter:call 里检查

Filter 只有 inner 和 predicate 两个字段,#[derive(Clone)]。Service 实现:

    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
        self.inner.poll_ready(cx).map_err(Into::into)
    }

    fn call(&mut self, request: Request) -> Self::Future {
        ResponseFuture::new(match self.predicate.check(request) {
            Ok(request) => Either::Right(self.inner.call(request).err_into()),
            Err(e) => Either::Left(std::future::ready(Err(e))),
        })
    }

ResponseFuture 是 Either<Ready<Result<R, BoxError>>, ErrInto<F, BoxError>> 外面包一层不透明类型:拒绝时是一个立即完成的错误,通过时是内层 future 加错误转换。没有新的状态机,也没有分配。

poll_ready 原样转发,所以先问的是内层,谓词要等到 call 才跑:调用方为一个最终会被拒绝的请求,同样要等内层就绪。被拒绝时内层已经就绪、但没有被 call,它的就绪状态留在 self.inner 里,下一个请求直接用。比如内层是 ConcurrencyLimit,它取到的许可还在 self.permit 里,不会丢,也不会多占。

ServiceBuilder::filter 和 FilterLayer 生成的就是 Filter。Filter 还把谓词单独暴露成 check 方法,不经过 call 也能检查一个请求。

AsyncFilter:检查放进 future

AsyncFilter 字段相同,Service 实现多了一个约束 T: Clone。call:

    fn call(&mut self, request: Request) -> Self::Future {
        use std::mem;

        let inner = self.inner.clone();
        // In case the inner service has state that's driven to readiness and
        // not tracked by clones (such as `Buffer`), pass the version we have
        // already called `poll_ready` on into the future, and leave its clone
        // behind.
        let inner = mem::replace(&mut self.inner, inner);

        // Check the request
        let check = self.predicate.check(request);

        AsyncResponseFuture::new(check, inner)
    }

异步检查要在 call 返回之后才完成,到那时再调用内层,future 手里就得有一个内层服务。&mut self 的借用活不到 future 里,所以只能 clone 一份放进去。放进去的必须是已经 poll_ready 过的那一份:clone 出来的副本不带就绪状态(注释点名了 Buffer,它的副本不带预留的通道位置)。mem::replace 把新 clone 留在 self.inner,把就绪的那个换出来交给 future。

AsyncResponseFuture 有两个状态,Check 和 WaitResponse,poll:

        loop {
            match this.state.as_mut().project() {
                StateProj::Check { mut check } => {
                    let request = ready!(check.as_mut().poll(cx))?;
                    let response = this.service.call(request);
                    this.state.set(State::WaitResponse { response });
                }
                StateProj::WaitResponse { response } => {
                    return response.poll(cx).map_err(Into::into);
                }
            }
        }

检查通过,在 future 里 call 内层;检查失败,? 直接返回错误,future 连同里面那个就绪的内层一起被 drop,内层占的东西(Buffer 的位置、ConcurrencyLimit 的许可)这时归还。代价也在这里:异步检查进行期间,这些东西一直被占着。

📝NOTE
没有蠢问题

问: Filter 不要求内层 Clone,为什么 AsyncFilter 要?

答: 区别在内层 call 发生在哪。Filter 的检查是同步的,在 call 里当场做完,当场用 &mut self.inner 调用内层。AsyncFilter 的检查要 .await,内层的 call 只能推迟到响应 future 里,而响应 future 不能借用 self,只能自己持有一个服务。持有的只能是从 self 换出来的那一个,self 又不能空着,所以要 clone 一个补回去。

实验:改写类型与拒绝

tower = { version = "=0.5.3", features = ["filter", "util"] }
tokio = { version = "=1.54.1", features = ["full", "test-util"] }
use std::num::ParseIntError;
use std::time::Duration;
use tower::{service_fn, BoxError, Service, ServiceBuilder, ServiceExt};

#[tokio::main(flavor = "current_thread", start_paused = true)]
async fn main() {
  // Inner service takes u32; the predicate turns a String into one.
  let double = service_fn(|n: u32| async move { Ok::<_, BoxError>(n * 2) });
  let mut svc = ServiceBuilder::new()
    .filter(|s: String| s.trim().parse::<u32>())
    .service(double);

  for input in ["21", "abc"] {
    match svc.ready().await.unwrap().call(input.to_string()).await {
      Ok(v) => println!("filter({input:?}) = {v}"),
      Err(e) => println!(
        "filter({input:?}) rejected: {e} | ParseIntError={}",
        e.downcast_ref::<ParseIntError>().is_some()
      ),
    }
  }

  // The async predicate may await before deciding.
  let echo = service_fn(|s: String| async move { Ok::<_, BoxError>(s) });
  let mut svc = ServiceBuilder::new()
    .filter_async(|user: String| async move {
      tokio::time::sleep(Duration::from_millis(5)).await; // e.g. ask a remote ACL
      if user == "mallory" {
        Err::<String, BoxError>(format!("{user} is banned").into())
      } else {
        Ok(format!("hello, {user}"))
      }
    })
    .service(echo);

  for user in ["alice", "mallory"] {
    let r = svc.ready().await.unwrap().call(user.to_string()).await;
    println!("filter_async({user:?}) = {:?}", r.map_err(|e| e.to_string()));
  }
}

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

filter("21") = 42
filter("abc") rejected: invalid digit found in string | ParseIntError=true
filter_async("alice") = Ok("hello, alice")
filter_async("mallory") = Err("mallory is banned")

谓词 s.trim().parse::<u32>() 返回 Result<u32, ParseIntError>,内层收到的是 u32。拒绝的错误被装进 BoxError,原始类型还在,downcast_ref::<ParseIntError>() 能取回来。异步谓词也把请求改写了("alice" 变成 "hello, alice")。

已知问题:AsyncFilter::layer 返回的是 FilterLayer

AsyncFilter::layer 的签名:

    pub fn layer(predicate: U) -> FilterLayer<U> {
        FilterLayer::new(predicate)
    }

返回的是同步的 FilterLayer,它造出来的是 Filter,不是 AsyncFilter。异步谓词不满足 Predicate,造出来的服务根本不实现 Service:

use tower::filter::AsyncFilter;
use tower::{service_fn, BoxError, Layer, Service, ServiceExt};

#[tokio::main(flavor = "current_thread")]
async fn main() {
  let echo = service_fn(|s: String| async move { Ok::<_, BoxError>(s) });
  let layer = AsyncFilter::<(), _>::layer(|s: String| async move { Ok::<_, BoxError>(s) });
  let mut svc = layer.layer(echo);
  let _ = svc.ready().await.unwrap().call("hi".to_string()).await;
}

rustc 1.99.0 的报错(开头部分):

error[E0599]: the method `ready` exists for struct `Filter<ServiceFn<{closure@...}>, {closure@...}>`, but its trait bounds were not satisfied
  --> examples/async_filter_layer.rs:9:15
   |
 9 |   let _ = svc.ready().await.unwrap().call("hi".to_string()).await;
   |               ^^^^^ method cannot be called on `Filter<ServiceFn<{closure@...}>, {closure@...}>` due to unsatisfied trait bounds

这是 issue #597(2021 年报告,「This seems to be a copy/paste mistake」),修复的 PR #875 写这一页时还没有合并,因为改返回类型算破坏性变更。0.5.3 里要用 AsyncFilterLayer::new 或 ServiceBuilder::filter_async,两者都正确地造出 AsyncFilter。

AsyncFilter 在 tower 内部也有用户:Hedge 用它判断一个请求能不能对冲,见第 06 部分。

小结

  • 谓词拿走请求,返回 Ok(新请求) 或 Err;关联类型 Request 让检查顺带改写请求、甚至换类型。闭包自动实现两个谓词 trait,错误装箱成 BoxError,可以 downcast 回原类型。
  • Filter 的 poll_ready 原样转发,call 里同步检查;拒绝时内层的就绪状态留在服务身上。响应 future 是一个 Either,没有额外状态。
  • AsyncFilter 把检查放进响应 future,通过后在 future 里调用内层;为此要求内层 Clone,并用 mem::replace 把已就绪的那个换进 future。检查期间内层占的资源一直被占着。
  • 0.5.3 的 AsyncFilter::layer 返回错了类型(#597),用 AsyncFilterLayer 或 ServiceBuilder::filter_async。

💬 关联讨论

发起讨论
加载中...