likes
comments
collection
share

rust中间层设计,告别tower,RPITIT值得你拥有更简洁的方案

作者站长头像
站长
· 阅读数 4

前言

rust中最常用的层次设计框架,当属tower。被广泛集成在各种框架中,比如rust中几乎所有web服务的都用到的hyper

tower框架,初看很简单,只有ServicLayer两个抽象,再细一看,what?这是21世纪程序员能搞出来的设计?

但仔细一琢磨,好像也只能这样干,在很长的时间里,大家只能捏着鼻子认了。(每次用这个东西,我都骂骂咧咧,你让我写,我也写不出更好的)

但是上个月rust在23年最后一个版本推出了RPITIT。这是个啥东西哪?我们下文再说。这个特性稳定后,中间层的设计将会非常方便。

tower

俗话说,没有对别就没有伤害,先看看tower中service是怎么抽象的,下面这一坨我都不想逐一字段讲是干嘛的。 整个中间层的设计和异步模式耦合严重,一眼看上去根本不能突出重点。徒增大量工作。

pub trait Service<Request> {
    type Response; //返回值类型
    type Error; //错误类型
    type Future: Future<Output = Result<Self::Response, Self::Error>>; //异步类型
    
    //询问是否准备ok
    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>>; 
    //调用
    fn call(&mut self, req: Request) -> Self::Future;
}

再看看具体用法,我们这里贴一段tower自己实现的超时Service。 这几乎是最简洁的例子了,仍然需要一大堆声明,在实际体验中,对齐类型就非常费劲,完全不符合我心目中整洁的rust形象。

#[derive(Debug, Clone)]
pub struct Timeout<T> {
    inner: T,
    timeout: Duration,
}

impl<S, Request> Service<Request> for Timeout<S>
where
    S: Service<Request>,
    S::Error: Into<crate::BoxError>,
{
    type Response = S::Response;
    type Error = crate::BoxError;
    type Future = ResponseFuture<S::Future>;

    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
        match self.inner.poll_ready(cx) {
            Poll::Pending => Poll::Pending,
            Poll::Ready(r) => Poll::Ready(r.map_err(Into::into)),
        }
    }
    fn call(&mut self, request: Request) -> Self::Future {
        let response = self.inner.call(request);
        let sleep = tokio::time::sleep(self.timeout);

        ResponseFuture::new(response, sleep)
    }
}

#[derive(Debug, Clone)]
pub struct TimeoutLayer {
    timeout: Duration,
}

impl TimeoutLayer {
    /// Create a timeout from a duration
    pub fn new(timeout: Duration) -> Self {
        TimeoutLayer { timeout }
    }
}

impl<S> Layer<S> for TimeoutLayer {
    type Service = Timeout<S>;

    fn layer(&self, service: S) -> Self::Service {
        Timeout::new(service, self.timeout)
    }
}

RPITIT

RPITIT是rust在1.75版本中发布的最新的特性(传送门),功能是 允许在trait中使用impl trait

举个例子:在下面的代码中,我们返回一个迭代器traitimpl Iterator,不需要指定它是内部type,也不需要指定它是泛型。就像在普通方法那样加一个impl参数,非常的好用。

trait Container { 
    fn items(&self) -> impl Iterator<Item = Widget>; 
}

那异步特征也就是Future,同样可以被这样使用, 当然,rust团队给了一个语法糖,可以用async fn代替-> impl Future<Output=XX>

我们对上面的Service进行一下重新抽象,会发现变的简单很多:

trait Service<Req,Resp>{
    async fn call(&self, req: Req) -> Resp;
}

实现一个简单的洋葱模型

tower就是一个典型的洋葱模型,就是一层套一层。调用的时候一层一层进去,再一层一层出来。

为了实现一个简单的洋葱模型,光有service是不行的,我们也抽象一个类似tower的Layer:

trait Layer<S,T>{
    fn layer(self,svc:S)->T;
}

有了Layer之后,就可以构造一个组装器:

struct ServiceBuilder<S>{
    inner: S,
}

impl<S> ServiceBuilder<S>{
    //创建
    fn new<Req,Resp>(inner:S)->Self
    where S:Service<Req,Resp>
    {
        ServiceBuilder {inner}
    }
    
    //用Layer特征来组装
    fn layer<Req,Resp,L,T>(self,l:L)-> ServiceBuilder<T>
    where S:Service<Req,Resp>,T:Service<Req,Resp>,L:Layer<S,T>,
    {
        let inner = l.layer(self.inner);
        ServiceBuilder {inner}
    }
    
    //构建完成,将Service吐出来
    fn build<Req,Resp>(self)->S
        where S:Service<Req,Resp>
    {
        self.inner
    }
    
    //允许直接调起执行
    async fn call<Req,Resp>(self,req:Req)->Resp
        where S:Service<Req,Resp>
    {
        self.inner.call(req).await
    }
}

实现一个日志中间层,和构建器

struct LogMiddle<S>{
    svc:S,
    log:String
}

impl<S,Req,Resp> Service<Req,Resp> for LogMiddle<S>
where S:Service<Req,Resp>
{
    async fn call(&self, req: Req) -> Resp {
        println!("start {} --->",self.log);
        let resp = self.svc.call(req).await;
        println!("end   {} <---",self.log);
        resp
    }
}

struct LogMiddleLayer{
    log:String,
}
impl<S> Layer<S,LogMiddle<S>> for LogMiddleLayer{
    fn layer(self, svc: S) -> LogMiddle<S> {
        LogMiddle{svc,log:self.log}
    }
}

再来一个超时的中间池和构建器

struct Timeout<S>{
    svc:S,
    sec:u64
}
impl<S,Req,Resp> Service<Req,Resp> for Timeout<S>
    where S:Service<Req,Resp>
{
    async fn call(&self, req: Req) -> Resp {
        println!("timeout{} sec",self.sec);
        if let Ok(resp) = tokio::time::timeout(Duration::from_secs(self.sec), self.svc.call(req)).await{
            return resp;
        }else{
            panic!("timeout")
        }
    }
}
struct TimeoutLayer{
    sec:u64
}
impl<S> Layer<S,Timeout<S>> for TimeoutLayer{
    fn layer(self, svc: S) -> Timeout<S> {
        Timeout{svc,sec:self.sec}
    }
}

调用

在真正的调用之前,先为service实现lambda表达式支持,方便直接写service实现。

impl<Req,Resp,F,Fut> Service<Req,Resp> for F
    where F:Fn(Req)->Fut,
        Fut:Future<Output=Resp>,
{
    async fn call(&self, req: Req) -> Resp {
        let future = self(req);
        let resp = future.await;
        return resp
    }
}

如下,进行一个简单的组装和调用:

#[tokio::main]
async fn main() {
    ServiceBuilder::new(|req:u64|async move{
        println!("exec {} second",req);
        tokio::time::sleep(Duration::from_secs(req)).await;
    })
        .layer(LogMiddleLayer{log:"log middle".into()})
        .layer(TimeoutLayer{sec:2})
        .call(1).await;
}

如此,是不是简单方便了很多。

尾语

  1. RPITIT 不是银弹, 当前还有很多缺陷。它不是object safe,所以不能动态调用。也不能添加边界,比如-> impl Future<Outpt=XX> + Send 这是不允许的,当然你一定要加,可以用#[trait_variant::make]

  2. #[async_trait] 还要不要用? 我的建议是,先用着,让子弹飞一会。但是,如果你和我一样属螃蟹的,可以直接用最新特性。老代码做个兼容。

  3. tower还用不用? 如果有框架上的依赖,比如你用hyper,或者势单力薄,写不了那么多中间层,那接着用。反之,你还是不要折磨自己,远离tower,自己实现一套。当然如果有一天tower升级到这个特性了,那当我没说。

最后,祝大家新年快乐,也祝自己不被裁员(回家前被通知组织调整)^(* ̄(oo) ̄)^