晚上好 世界

晚上好 世界

早安

VS Code 的 Code Lens 是一项增强代码编辑体验的功能,它会在代码中直接嵌入动态的、可交互的上下文信息(如引用、测试状态、Git 变更等),帮助开发者快速获取关键信息而无需跳转界面。以下是详细说明:


核心功能

  1. 引用显示(References)

    • 在函数、类或变量上方显示被引用的次数(例如:• 12 references),点击后可查看所有引用位置。
    • 作用:快速了解代码被哪些部分依赖。
  2. 测试状态(Test)

    • 如果项目有测试框架(如 Jest、Mocha),Code Lens 会显示测试用例的运行状态(例如:▶ Run Test | ✓ Passed)。
    • 作用:直接运行或调试测试,无需切换文件。
  3. Git 历史(GitLens 扩展)

    • 通过安装 GitLens 扩展,Code Lens 会显示代码行的最近修改作者、时间和提交信息。
    • 作用:追踪代码变更历史,辅助协作。
  4. 实现与继承(Implementations)

    • 对于接口或抽象类,显示实现该接口的子类数量(例如:• 3 implementations)。

如何启用/关闭

  • 默认状态:Code Lens 默认开启,但部分功能(如 Git 历史)需安装扩展。
  • 手动配置
    在设置中搜索 editor.codeLens

    • "editor.codeLens": true/false(全局开关)。
    • 针对语言单独配置(如 "[python]": { "editor.codeLens": false })。

优点

  • 减少上下文切换:直接查看关键信息,无需跳转到其他面板。
  • 提升效率:快速运行测试、查看引用或 Git 记录。
  • 可定制性:通过扩展增强功能(如 GitLens、Test Adapters)。

相关拓展

  • 插件市场搜索 Lens

2025-06-08T13:26:07.png

Error Lens

2025-06-08T13:26:40.png

配置说明

Version Lens

2025-06-08T13:31:14.png

Hover Lens

2025-06-08T13:30:12.png


转载自 https://github.com/tower-rs/tower/blob/master/guides/building-a-middleware-from-scratch.md

《发明Service trait》一文中,我们深入探讨了Service的设计动机及其架构原理。虽然我们也动手实现过几个简易中间件,但当时采取了一些取巧方案。本指南将完整构建Tower现有的Timeout中间件,全程不采用任何捷径。

编写健壮的中间件需要运用比常规更底层的异步Rust技术。本指南旨在揭开这些核心概念与模式的神秘面纱,助你掌握中间件开发技能,甚至为Tower生态贡献代码!

准备工作

我们将构建的中间件是tower::timeout::Timeout。该组件会限定内部Service响应future的最大执行时长。若未能在指定时间内生成响应,则返回错误。这使得客户端可以重试请求或向用户报错,而非无限等待。

首先定义包含被包装服务和超时时长的Timeout结构体:

use std::time::Duration;

struct Timeout<S> {
    inner: S,
    timeout: Duration,
}

根据《发明Service trait》的指导,服务实现Clone trait至关重要——这允许将Service::call接收的&mut self转换为可移入响应future的独立所有权。因此我们为结构体添加#[derive(Clone)],同时一并实现Debug

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

接着实现构造函数:

impl<S> Timeout<S> {
    pub fn new(inner: S, timeout: Duration) -> Self {
        Timeout { inner, timeout }
    }
}

注意我们遵循Rust API指南的建议,即便预期S会实现Service trait,此处也未添加任何约束。

现在进入关键环节:如何为Timeout<S>实现Service?先实现一个简单透传版本:

use tower::Service;
use std::task::{Context, Poll};

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

    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
        // 中间件不关注背压,只要内部服务就绪即可
        self.inner.poll_ready(cx)
    }

    fn call(&mut self, request: Request) -> Self::Future {
        self.inner.call(request)
    }
}

在熟练编写中间件前,先搭建这样的代码骨架能显著降低实现难度。

要实现真正的超时控制,核心在于检测self.inner.call(request)返回的future执行是否超过self.timeout,若超时则终止并返回错误。

我们将采用以下方案:调用tokio::time::sleep获取超时future,然后通过select等待最先完成的future。虽然也可使用tokio::time::timeout,但sleep同样适用。

创建两个future的代码如下:

use tokio::time::sleep;

fn call(&mut self, request: Request) -> Self::Future {
    let response_future = self.inner.call(request);

    // 此变量类型为`tokio::time::Sleep`
    // 由于`self.timeout`实现`Copy` trait,无需显式克隆
    let sleep = tokio::time::sleep(self.timeout);

    // 此处应返回什么?
}

一种可能的返回类型是Pin<Box<dyn Future<...>>>。但为最小化Timeout的开销,我们希望能避免Box分配。设想一个包含数十层嵌套Service的调用栈,若每层都为请求分配新Box,将产生大量内存分配,进而影响性能1

响应future实现

为避免Box分配,我们选择自定义Future实现。首先创建名为ResponseFuture的结构体,需泛型化内部服务的响应future类型。这类似于用服务包装其他服务,但此处是用future包装其他future。

use tokio::time::Sleep;

pub struct ResponseFuture<F> {
    response_future: F,
    sleep: Sleep,
}

其中F对应self.inner.call(request)的类型。更新Service实现:

impl<S, Request> Service<Request> for Timeout<S>
where
    S: Service<Request>,
{
    type Response = S::Response;
    type Error = S::Error;

    // 使用新的`ResponseFuture`类型
    type Future = ResponseFuture<S::Future>;

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

    fn call(&mut self, request: Request) -> Self::Future {
        let response_future = self.inner.call(request);
        let sleep = tokio::time::sleep(self.timeout);

        // 通过包装内部服务的future创建响应future
        ResponseFuture {
            response_future,
            sleep,
        }
    }
}

这里的关键在于Rust future具有_惰性_特性,即除非被await或poll,否则不会执行任何操作。因此self.inner.call(request)会立即返回而不会实际处理请求。

接下来为ResponseFuture实现Future

use std::{pin::Pin, future::Future};

impl<F, Response, Error> Future for ResponseFuture<F>
where
    F: Future<Output = Result<Response, Error>>,
{
    type Output = Result<Response, Error>;

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        // 此处如何实现?
    }
}

理想情况下我们期望实现以下逻辑:

  1. 首先poll self.response_future,若就绪则返回其响应或错误
  2. 否则poll self.sleep,若就绪则返回超时错误
  3. 若两者均未就绪则返回Poll::Pending

初步尝试可能如下:

fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
    match self.response_future.poll(cx) {
        Poll::Ready(result) => return Poll::Ready(result),
        Poll::Pending => {}
    }

    todo!()
}

但这会导致如下错误:

error[E0599]: 在当前作用域中未找到类型参数`F`的`poll`方法
  --> src/lib.rs:56:29
   |
56 |         match self.response_future.poll(cx) {
   |                             ^^^^ 方法未找到
   |
   = 帮助: 只有类型参数受trait约束时才能使用trait中的项
帮助: 以下trait定义了`poll`项,可能需要为类型参数`F`添加约束:
   |
49 | impl<F: Future, Response, Error> Future for ResponseFuture<F>
   |      ^^^^^^^^^

虽然Rust的错误提示建议添加F: Future约束,但实际上我们已通过where F: Future<Output = Result<Response, E>>实现约束。真正的问题与[Pin]相关。

关于固定(Pinning)的完整讨论超出本指南范围。若对Pin不熟悉,推荐阅读Jon Gjengset的《Rust中Pinning的为什么、是什么和怎么做》

Rust试图告诉我们的是:需要Pin<&mut F>才能调用poll。当selfPin<&mut Self>时,通过self.response_future访问F无法正常工作。

我们需要"pin投影"——即从Pin<&mut Struct>转换到Pin<&mut Field>。通常这需要编写unsafe代码,但优秀的[pin-project] crate能安全处理这些底层细节。

使用pin-project时,我们用#[pin_project]标注结构体,并为需要固定访问的字段添加#[pin]

use pin_project::pin_project;

#[pin_project]
pub struct ResponseFuture<F> {
    #[pin]
    response_future: F,
    #[pin]
    sleep: Sleep,
}

impl<F, Response, Error> Future for ResponseFuture<F>
where
    F: Future<Output = Result<Response, Error>>,
{
    type Output = Result<Response, Error>;

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        // 调用`#[pin_project]`生成的`project`魔法方法
        let this = self.project();

        // `project`返回`__ResponseFutureProjection`类型(可忽略具体类型)
        // 其字段与`ResponseFuture`匹配,并为标注`#[pin]`的字段维护pin

        // `this.response_future`现在是`Pin<&mut F>`
        let response_future: Pin<&mut F> = this.response_future;

        // `this.sleep`是`Pin<&mut Sleep>`
        let sleep: Pin<&mut Sleep> = this.sleep;

        // 若有未标注`#[pin]`的字段,则获得普通`&mut`引用(无`Pin`)

        // ...
    }
}

Rust中的固定机制虽然复杂难懂,但借助pin-project我们可以规避大部分复杂性。关键在于,即使不完全理解固定机制,也能编写Tower中间件!所以如果你对PinUnpin还存有疑惑,请放心使用pin-project!

注意在前述代码中,我们获得了Pin<&mut F>Pin<&mut Sleep>,这正是调用poll所需的:

impl<F, Response, Error> Future for ResponseFuture<F>
where
    F: Future<Output = Result<Response, Error>>,
{
    type Output = Result<Response, Error>;

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        let this = self.project();

        // 首先检查响应future是否就绪
        match this.response_future.poll(cx) {
            Poll::Ready(result) => {
                // 内部服务已准备好响应或失败
                return Poll::Ready(result);
            }
            Poll::Pending => {
                // 尚未就绪...
            }
        }

        // 然后检查sleep是否就绪。若就绪则表示响应超时
        match this.sleep.poll(cx) {
            Poll::Ready(()) => {
                // 超时触发,但应返回什么错误?!
                todo!()
            }
            Poll::Pending => {
                // 仍有剩余时间...
            }
        }

        // 若两者均未就绪,则返回Pending
        Poll::Pending
    }
}

现在唯一剩下的问题是:当sleep先完成时,应该返回什么错误?

错误类型设计

当前我们承诺返回的泛型Error类型与内部服务的错误类型相同。但对该类型我们一无所知——它完全不透明且无法构造其值。

我们有三条路径可选:

  1. 返回装箱的错误特征对象,如Box<dyn std::error::Error + Send + Sync>
  2. 返回包含服务错误和超时错误的枚举类型
  3. 定义TimeoutError结构体,并要求泛型错误类型可通过TimeoutError: Into<Error>构造

虽然选项3看似最灵活,但要求使用自定义错误类型的用户手动实现From<TimeoutError> for MyError。当使用多个自带错误类型的中间件时,这种操作会变得繁琐。

选项2需要定义如下枚举:

enum TimeoutError<Error> {
    // 超时触发的变体
    Timeout(InnerTimeoutError),
    // 内部服务产生错误的变体
    Service(Error),
}

虽然表面上看能保留完整类型信息且可通过match精确处理错误,但存在三个问题:

  1. 实践中常会嵌套大量中间件,导致最终错误枚举异常庞大。类似BufferError<RateLimitError<TimeoutError<MyError>>>的类型很常见,对此类类型进行模式匹配(例如判断错误是否可重试)将非常繁琐
  2. 调整中间件顺序会改变最终错误类型,需要同步更新模式匹配
  3. 最终错误类型可能占用大量栈空间

因此我们选择选项1:将内部服务错误转换为装箱特征对象Box<dyn std::error::Error + Send + Sync>。这样可将多种错误类型统一处理,具有以下优势:

  1. 错误处理更健壮,调整中间件顺序不会改变最终错误类型
  2. 错误类型具有固定大小,不受中间件数量影响
  3. 提取错误时无需大型match,可使用error.downcast_ref::<Timeout>()

但也存在以下缺点:

  1. 使用动态转换后,编译器无法保证检查所有可能的错误类型
  2. 创建错误需要进行内存分配。实践中错误应属罕见,故影响有限

选择哪种方案取决于个人偏好。Tower最终采用的是装箱特征对象方案,原始讨论参见这里

对于Timeout中间件,我们需要创建实现std::error::Error的结构体,以便转换为Box<dyn std::error::Error + Send + Sync>。同时要求内部服务的错误类型实现Into<Box<dyn std::error::Error + Send + Sync>>。幸运的是大多数错误类型自动满足该条件,用户无需编写额外代码。根据标准库建议,我们使用Into而非From作为trait约束。

错误类型实现如下:

use std::fmt;

#[derive(Debug, Default)]
pub struct TimeoutError(());

impl fmt::Display for TimeoutError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.pad("request timed out")
    }
}

impl std::error::Error for TimeoutError {}

我们向 TimeoutError 添加了一个私有字段,这样Tower外部的用户就无法构造自己的TimeoutError 。他们只能通过我们的中间件获取。

Box<dyn std::error::Error + Send + Sync> 这个表达确实有些冗长,因此我们为它定义一个类型别名:

// 该类型在`tower`中定义为`tower::BoxError`
pub type BoxError = Box<dyn std::error::Error + Send + Sync>;

现在future实现更新为:

impl<F, Response, Error> Future for ResponseFuture<F>
where
    F: Future<Output = Result<Response, Error>>,
    // 要求内部服务错误可转换为`BoxError`
    Error: Into<BoxError>,
{
    type Output = Result<
        Response,
        // `ResponseFuture`的错误类型现在是`BoxError`
        BoxError,
    >;

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        let this = self.project();

        match this.response_future.poll(cx) {
            Poll::Ready(result) => {
                // 使用`map_err`转换错误类型
                let result = result.map_err(Into::into);
                return Poll::Ready(result);
            }
            Poll::Pending => {}
        }

        match this.sleep.poll(cx) {
            Poll::Ready(()) => {
                // 构造并返回超时错误
                let error = Box::new(TimeoutError(()));
                return Poll::Ready(Err(error));
            }
            Poll::Pending => {}
        }

        Poll::Pending
    }
}

最后需要更新Service实现,同样使用BoxError

impl<S, Request> Service<Request> for Timeout<S>
where
    S: Service<Request>,
    // 与`ResponseFuture`的future实现相同约束
    S::Error: Into<BoxError>,
{
    type Response = S::Response;
    // `Timeout`的错误类型现在是`BoxError`
    type Error = BoxError;
    type Future = ResponseFuture<S::Future>;

    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 {
        let response_future = self.inner.call(request);
        let sleep = tokio::time::sleep(self.timeout);

        ResponseFuture {
            response_future,
            sleep,
        }
    }
}

最终成果

至此我们已成功实现了与Tower现有版本完全一致的Timeout中间件!

完整实现如下:

use pin_project::pin_project;
use std::time::Duration;
use std::{
    fmt,
    future::Future,
    pin::Pin,
    task::{Context, Poll},
};
use tokio::time::Sleep;
use tower::Service;

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

impl<S> Timeout<S> {
    fn new(inner: S, timeout: Duration) -> Self {
        Timeout { inner, timeout }
    }
}

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

    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 {
        let response_future = self.inner.call(request);
        let sleep = tokio::time::sleep(self.timeout);

        ResponseFuture {
            response_future,
            sleep,
        }
    }
}

#[pin_project]
struct ResponseFuture<F> {
    #[pin]
    response_future: F,
    #[pin]
    sleep: Sleep,
}

impl<F, Response, Error> Future for ResponseFuture<F>
where
    F: Future<Output = Result<Response, Error>>,
    Error: Into<BoxError>,
{
    type Output = Result<Response, BoxError>;

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        let this = self.project();

        match this.response_future.poll(cx) {
            Poll::Ready(result) => {
                let result = result.map_err(Into::into);
                return Poll::Ready(result);
            }
            Poll::Pending => {}
        }

        match this.sleep.poll(cx) {
            Poll::Ready(()) => {
                let error = Box::new(TimeoutError(()));
                return Poll::Ready(Err(error));
            }
            Poll::Pending => {}
        }

        Poll::Pending
    }
}

#[derive(Debug, Default)]
struct TimeoutError(());

impl fmt::Display for TimeoutError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.pad("request timed out")
    }
}

impl std::error::Error for TimeoutError {}

type BoxError = Box<dyn std::error::Error + Send + Sync>;

Tower中的完整实现参见这里

这种为包装其他Service的类型实现Service trait,并返回包装其他FutureFuture的模式,是大多数Tower中间件的工作方式。

其他典型示例包括:

掌握这些知识后,你应该已具备编写生产级中间件的能力。以下练习可供实践:

  • 使用tokio::time::timeout重新实现超时中间件
  • 实现类似Result::mapResult::map_errService适配器,通过用户提供的闭包转换请求、响应或错误
  • 实现ConcurrencyLimit(提示:需要PollSemaphore来实现poll_ready

如有疑问,欢迎加入Tokio Discord服务器#tower频道交流。


  1. Rust编译器团队计划增加"类型别名中的impl Trait"功能,将允许从call返回impl Future,但目前尚未实现。

文章转载并翻译自:https://heikoseeberger.de/2022/10/20/2022-10-21-tower-1/

第一讲:将服务视为函数

作者:Heiko Seeberger 2022年10月21日 标签:Rust, Tokio, Tower

当我第一次接触Tower库(Tokio技术栈的一部分)时,注意到它的标题行写着:

async fn(Request) -> Result<Response, Error>

文档中对此解释如下:

Tower提供了一个简单的核心抽象——Service特性,它表示一个异步函数,接收请求并返回响应或错误。这个抽象可用于建模客户端和服务器。

这立即让我这个经验丰富的Scala开发者想起了Finagle论文《Your Server as a Function》,其中定义"系统边界由称为服务的异步函数表示"。看到新项目利用已有知识总是件好事。

虽然Tower和Finagle服务共享相同的核心抽象——异步函数,但有两点不同。

首先,Tower不一定将服务解释为系统边界。相反,服务也可以"本地"组合,这更符合Finagle的"过滤器":

通用组件(如超时、速率限制和负载平衡)可以建模为包装某些内部服务并在调用内部服务前后应用附加行为的服务。这允许以协议无关、可组合的方式实现这些组件。通常,此类服务被称为中间件。

其次,Tower通过就绪概念丰富了异步函数的核心抽象,正如我们从Service特性的定义中看到的:

pub trait Service<Request> {
    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: Request) -> Self::Future;
}

其核心在于这些服务方法的调用契约如下:

  1. 必须先调用poll_ready(可能需要多次)直到返回Poll::Ready
  2. 然后才能调用call
  3. 如果不遵守上述规则,call可能会panic

在下一讲深入探讨调用服务的细节之前,我们先看一个简单的服务示例:

pub struct EchoRequest(String);
pub struct EchoResponse(String);
pub struct EchoService;

/// 为[EchoService]实现Tower的`Service`特性:
/// 总是就绪,永不失败,立即返回回显响应(即内容与请求相同的响应)
impl Service<EchoRequest> for EchoService {
    type Response = EchoResponse;
    type Error = Infallible; // 此服务永不失败
    type Future = Ready<Result<Self::Response, Self::Error>>; // 立即响应

    /// 总是返回`Poll::Ready`:此服务总是就绪
    fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
        Poll::Ready(Ok(()))
    }

    /// 总是返回回显,即内容与请求相同的响应
    fn call(&mut self, req: EchoRequest) -> Self::Future {
        ready(Ok(EchoResponse(req.0)))
    }
}

由于此服务总是就绪的,因此如果之前没有调用poll_readycall也不需要panic。

完整代码可在GitHub的tower-experiments仓库找到。下一讲我们将探讨服务客户端。

第二讲:调用Tower服务

作者:Heiko Seeberger 2022年10月23日 标签:Rust, Tokio, Tower

在上一讲中我们了解到,Tower服务由poll_readycall两个方法组成,它们遵循这样的契约:调用者必须先持续调用poll_ready直到返回Poll::Ready,然后才能调用call,否则call可能会panic。

poll_ready的签名如下:

fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>>;

要调用poll_ready,我们需要一个std::task::Context。如果你熟悉Future,会发现它的poll方法也有这样的参数。那么我们是否必须实现一个Future才能调用服务呢?

幸运的是不必,因为Tower已经提供了ServiceExt特性,其中包含一些辅助方法,可以按照契约要求调用poll_ready,甚至链式调用poll_readycall

首先我们来看ServiceExt::oneshot。根据文档说明,它会"消费此服务,在就绪时使用提供的请求调用一次":

let mut service = EchoService;
let response = service.oneshot("Hello, Tower!".into()).await?;
println!("Echo service responded with: {response}");

oneshot返回的Future在被轮询时首先调用poll_ready,当服务返回Poll::Ready时再调用call。当然,由于oneshot通过self消费了服务,所以不可能再次调用该服务。但这实际上是Tower提供的唯一能强制执行poll_readycall之间契约的方式。

接下来看看ServiceExt::ready,根据文档说明,它"在服务准备好接受请求时返回一个可变引用":

let mut service = EchoService;

let service = service.ready().await?;
let response = service.call("Hello, Tower!".into()).await?;
println!("Echo service responded with: {response}");

// 再次调用call前应该先调用ready!
// service.ready().await?;
let response = service.call("Hello again, Tower!".into()).await?;
println!("Echo service responded with: {response}");

ready返回的Future会返回服务的可变引用,然后可以用来调用它。如你所见,我们甚至可以不检查就绪状态就多次调用服务。当然这违反了契约,我们应该在每次调用call之前再次调用ready,即在上面的代码片段中应该取消第8行的注释。

最后还有ServiceExt::ready_oneshot。它像ready一样工作,但会消费服务,然后又将其返回:

let service = EchoService;

let mut service = service.ready_oneshot().await?;
let response = service.call("Hello, Tower!".into()).await?;
println!("Echo service responded with: {response}");

// 再次调用call前应该先调用ready_oneshot!
// let mut service = service.ready_oneshot().await?;
let response = service.call("Hello again, Tower!".into()).await?;
println!("Echo service responded with: {response}");

ready类似,我们可以不检查就绪状态就多次调用返回的服务。因此我认为这个方法命名不当,因为"oneshot"对我来说意味着最多只能调用一次call

现在你可能会问,这种总是先调用poll_ready再调用call的服务契约是否真的那么重要。对于无害的EchoService来说确实不重要,因为它总是就绪的,所以它的call方法永远不会panic。我们将在下一讲中看看其他表现不那么好的服务。完整代码照例可在GitHub的tower-experiments仓库找到。

第三讲:就绪状态

作者:Heiko Seeberger 2022年11月8日 标签:Rust, Tokio, Tower

在上一讲中,我们学习了如何调用Tower服务。特别讨论了poll_readycall之间的契约关系:调用者必须首先持续调用poll_ready直到返回Poll::Ready,才能调用call方法,否则call可能会触发panic。

当然,某些服务(例如第一讲中介绍的EchoService)始终处于就绪状态,因此无需调用poll_ready直接调用call也能正常工作。

但通常情况下,服务的内部状态或外部依赖(例如数据库连接)会影响其就绪状态。此外,将领域服务与中间件(如限流器或熔断器)组合时,自然也会影响就绪状态。

为了演示这一点,我们来看一个在"就绪"与"未就绪"状态间交替切换的服务:

/// 交替就绪服务,接收[AlternatingReadyRequest]并返回[AlternatingReadyResponse]
#[derive(Debug, Clone)]
pub struct AlternatingReadyService {
    ready: bool,
}

/// 为[AlternatingReadyService]实现Tower的`Service`特性:
/// 就绪状态在Pending和Ready间交替切换,调用永不失败且响应立即就绪
impl Service<AlternatingReadyRequest> for AlternatingReadyService {
    type Response = AlternatingReadyResponse;
    type Error = Infallible; // 该服务永不失败
    type Future = Ready<Result<Self::Response, Self::Error>>; // 响应立即就绪

    /// 交替返回`Poll::Pending`和`Poll::Ready`
    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
        if self.ready {
            // 唤醒任务确保执行器再次调用poll_ready
            cx.waker().wake_by_ref();
            self.ready = false;
            Poll::Pending
        } else {
            self.ready = true;
            Poll::Ready(Ok(()))
        }
    }

    /// 始终返回[AlternatingReadyResponse]
    /// 
    /// # Panics
    /// 若未先调用poll_ready就直接调用本方法会panic
    fn call(&mut self, _req: AlternatingReadyRequest) -> Self::Future {
        if !self.ready {
            panic!("服务未就绪,必须先调用poll_ready");
        }
        self.ready = false;
        ready(Ok(AlternatingReadyResponse))
    }
}

AlternatingReadyService通过ready字段跟踪就绪状态。poll_ready的实现逻辑是:若服务之前处于就绪状态,则将其设为未就绪并返回Poll::Pending;反之则设为就绪并返回Poll::Ready。同时,call方法会检查ready字段,若服务未就绪时调用将触发panic。

虽然这个实现是刻意设计的,但它很好地演示了poll_readycall之间契约的重要性:

let mut service = AlternatingReadyService::new();

let service = service.ready().await?;
let _response = service.call(AlternatingReadyRequest).await?;

// 再次调用call前应该先调用ready!
// service.ready().await?;
let _response = service.call(AlternatingReadyRequest).await?;

在这个例子中,第二次call调用会因为服务处于未就绪状态而panic(由于第一次ready调用后服务状态变为未就绪)。如果你想亲自验证,可以在GitHub的tower-experiments仓库查看完整代码。


6月 - 8月 完成 BlogYou 项目
8月 - 12月 完成 用户中心、财务系统、服务大厅等基建项目

哦哦哦加油!!!


数据库拓展函数

Rust层((接收gRPC/http信息,调用下面的业务逻辑层)调用业务逻辑,CRUD之类的简单玩意直接生成SQL去调数据层)

业务逻辑层((用户增删、通过特定验证方式为用户鉴权)数据库拓展函数)

数据层(略)

2025-06-02T12:03:08.png

参考:https://supabase.com/docs/guides/database/functions?queryGroups=language&language=js