rust中间层设计,告别tower,RPITIT值得你拥有更简洁的方案
前言
rust中最常用的层次设计框架,当属tower
。被广泛集成在各种框架中,比如rust中几乎所有web服务的都用到的hyper
。
tower
框架,初看很简单,只有Servic
和Layer
两个抽象,再细一看,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;
}
如此,是不是简单方便了很多。
尾语
-
RPITIT 不是银弹, 当前还有很多缺陷。它不是
object safe
,所以不能动态调用。也不能添加边界,比如-> impl Future<Outpt=XX> + Send
这是不允许的,当然你一定要加,可以用#[trait_variant::make]
-
#[async_trait]
还要不要用? 我的建议是,先用着,让子弹飞一会。但是,如果你和我一样属螃蟹的,可以直接用最新特性。老代码做个兼容。 -
tower还用不用? 如果有框架上的依赖,比如你用
hyper
,或者势单力薄,写不了那么多中间层,那接着用。反之,你还是不要折磨自己,远离tower,自己实现一套。当然如果有一天tower升级到这个特性了,那当我没说。
最后,祝大家新年快乐,也祝自己不被裁员(回家前被通知组织调整)^(* ̄(oo) ̄)^
转载自:https://juejin.cn/post/7332734911232081929