< 返回版块

ohmycloud 发表于 2025-01-16 00:09

Tags:Rust,tokio,async,axum

求助社区的朋友们,我在使用 Axum 改写 《Zero To Production》 第三章 “SIGN UP A NEW SUBSCRIBER” 的代码时,碰到了一个问题,使用 cargo run 运行正常, 在浏览器中请求 http://127.0.0.1:3333/health_check 没有问题,使用 curl -v http://127.0.0.1:3333/health_check 请求 health_check 路由时也能正常返回。

但是使用 cargo test health_check_works 进行测试时,却把测试程序卡住了:

test health_check_works has been running for over 60 seconds

可能是哪里用的不对把程序阻塞了。我觉得这个问题蛮值得研究一下的,用 Claude/DeepSeek 帮助分析也没有解决。请大家帮助看看问题出在哪里。(可以克隆 https://github.com/ohmycloud/zero2prod-axum.git, 执行 cargo test health_check_works 复现该问题。)

tests/health_check.rs 的代码如下:

// `tokio::test` is the testing equivalent of `tokio::main`.
// Is also spares you from having to specify the `#[test]` attribute.
//
// You can inspect what code gets generated using
// `cargo expand --test health_check`
#[tokio::test]
async fn health_check_works() {
    // Arrange
    spawn_app();

    // We need to bring in `reqwest`
    // to perform HTTP requsts against our application.
    let client = reqwest::Client::new();

    // Act
    let response = client
        .get("http://127.0.0.1:3333/health_check")
        .send()
        .await
        .expect("Falied to execute reqwest.");

    // Assert
    assert!(response.status().is_success());
    assert_eq!(Some(0), response.content_length());
}

// Launch our application in the background
fn spawn_app() {
    let server = zero2prod::run()
        .expect("Failed to bind address")
        .into_future();
    let _ = tokio::spawn(server);
    println!("app spawned.");
}

src/lib.rs 中的代码如下:

pub mod health_check;

use crate::health_check::health_check;
use axum::{
    Router,
    extract::Path,
    response::IntoResponse,
    routing::{IntoMakeService, get},
    serve::Serve,
};
use tokio::net::TcpListener;

async fn index() -> impl IntoResponse {
    "Rust Rocks!"
}

async fn greet(Path(name): Path<String>) -> impl IntoResponse {
    format!("Hello, {}", name)
}

pub fn run() -> Result<Serve<TcpListener, IntoMakeService<Router>, Router>, std::io::Error> {
    let app = Router::new()
        .route("/health_check", get(health_check))
        .route("/", get(index))
        .route("/{name}", get(greet));

    let listener = std::net::TcpListener::bind("0.0.0.0:3333")?;
    let listener = TcpListener::from_std(listener)?;
    println!("Listening on {:?}", listener.local_addr());

    let server = axum::serve(listener, app.into_make_service());
    Ok(server)
}

src/health_check.rs 中的代码如下:

use axum::{http::Response, response::IntoResponse};

pub async fn health_check() -> impl IntoResponse {
    let response = Response::new("hello world");
    response.status()
}

src/main.rs 的代码如下:

use zero2prod::run;

#[tokio::main]
async fn main() -> Result<(), std::io::Error> {
    run()?.await
}

Ext Link: https://github.com/ohmycloud/zero2prod-axum.git

评论区

写评论
Bai-Jinlin 2025-01-16 21:52

我以前有个类似的bug,当时是flutter调用rust,我的逻辑是先创建std的listener,然后启动服务的时候用tokio listener from std,当时axum就在android正常,windows上卡住,不创建std 直接用tokio listener就没问题,原来是我没仔细看文档设置socket为non blocking。。。

--
👇
ohmycloud: 我还以为一个人哈哈哈哈哈,问题虽然小,很有帮助。谢谢烙铁。

--
👇
Lazy: 确实啊老铁仓库跟你贴出来的代码不一样,不过我看了一下,出现的问题都是一样的:

    // 标准库的TcpListener
    let listener = std::net::TcpListener::bind("0.0.0.0:3333")?;
    // 异步库的TcpListener
    let listener = TcpListener::from_std(listener)?;

你可能没注意到,标准库的TcpListener默认是阻塞的,所以服务的监听会直接阻塞线程,即使包装成异步的也不会改变其阻塞属性,后续的操作都无法执行的。

所以你需要:

    // 标准库的TcpListener
    let listener = std::net::TcpListener::bind("0.0.0.0:3333")?;
    // 设置为非阻塞模式,让异步包装可以非阻塞轮询
    listener.set_nonblocking(true).unwrap();
    // 异步库的TcpListener
    let listener = TcpListener::from_std(listener)?;

如果你有注意到tokio::net::TcpListener::from_std文档说明的话,应该也是能看到以下内容:

    ...
    /// # Notes
    ///
    /// The caller is responsible for ensuring that the listener is in
    /// non-blocking mode. Otherwise all I/O operations on the listener
    /// will block the thread, which will cause unexpected behavior.
    /// Non-blocking mode can be set using [`set_nonblocking`].
    ...

所以在我拉下你的代码库的时候,即使是执行的cargo run [-r],在浏览器里我也是访问阻塞的

作者 ohmycloud 2025-01-16 13:01

我还以为一个人哈哈哈哈哈,问题虽然小,很有帮助。谢谢烙铁。

--
👇
Lazy: 确实啊老铁仓库跟你贴出来的代码不一样,不过我看了一下,出现的问题都是一样的:

    // 标准库的TcpListener
    let listener = std::net::TcpListener::bind("0.0.0.0:3333")?;
    // 异步库的TcpListener
    let listener = TcpListener::from_std(listener)?;

你可能没注意到,标准库的TcpListener默认是阻塞的,所以服务的监听会直接阻塞线程,即使包装成异步的也不会改变其阻塞属性,后续的操作都无法执行的。

所以你需要:

    // 标准库的TcpListener
    let listener = std::net::TcpListener::bind("0.0.0.0:3333")?;
    // 设置为非阻塞模式,让异步包装可以非阻塞轮询
    listener.set_nonblocking(true).unwrap();
    // 异步库的TcpListener
    let listener = TcpListener::from_std(listener)?;

如果你有注意到tokio::net::TcpListener::from_std文档说明的话,应该也是能看到以下内容:

    ...
    /// # Notes
    ///
    /// The caller is responsible for ensuring that the listener is in
    /// non-blocking mode. Otherwise all I/O operations on the listener
    /// will block the thread, which will cause unexpected behavior.
    /// Non-blocking mode can be set using [`set_nonblocking`].
    ...

所以在我拉下你的代码库的时候,即使是执行的cargo run [-r],在浏览器里我也是访问阻塞的

作者 ohmycloud 2025-01-16 12:55

感谢老铁,跳转到 tokio 的 TcpListener::from_std 文档,里面还特意标注了 Notes , 写的明明白白: Non-blocking mode can be set using [set_nonblocking].

--
👇
Nayaka: 正如我所说:"用右手手指给右手手背抓痒",还有啊,2024下月才发布,咱先用2021吧

--
👇
Lazy: 确实啊老铁仓库跟你贴出来的代码不一样,不过我看了一下,出现的问题都是一样的:

    // 标准库的TcpListener
    let listener = std::net::TcpListener::bind("0.0.0.0:3333")?;
    // 异步库的TcpListener
    let listener = TcpListener::from_std(listener)?;

你可能没注意到,标准库的TcpListener默认是阻塞的,所以服务的监听会直接阻塞线程,即使包装成异步的也不会改变其阻塞属性,后续的操作都无法执行的。

所以你需要:

    // 标准库的TcpListener
    let listener = std::net::TcpListener::bind("0.0.0.0:3333")?;
    // 设置为非阻塞模式,让异步包装可以非阻塞轮询
    listener.set_nonblocking(true).unwrap();
    // 异步库的TcpListener
    let listener = TcpListener::from_std(listener)?;

如果你有注意到tokio::net::TcpListener::from_std文档说明的话,应该也是能看到以下内容:

    ...
    /// # Notes
    ///
    /// The caller is responsible for ensuring that the listener is in
    /// non-blocking mode. Otherwise all I/O operations on the listener
    /// will block the thread, which will cause unexpected behavior.
    /// Non-blocking mode can be set using [`set_nonblocking`].
    ...

所以在我拉下你的代码库的时候,即使是执行的cargo run [-r],在浏览器里我也是访问阻塞的

Nayaka 2025-01-16 11:49

正如我所说:"用右手手指给右手手背抓痒",还有啊,2024下月才发布,咱先用2021吧

--
👇
Lazy: 确实啊老铁仓库跟你贴出来的代码不一样,不过我看了一下,出现的问题都是一样的:

    // 标准库的TcpListener
    let listener = std::net::TcpListener::bind("0.0.0.0:3333")?;
    // 异步库的TcpListener
    let listener = TcpListener::from_std(listener)?;

你可能没注意到,标准库的TcpListener默认是阻塞的,所以服务的监听会直接阻塞线程,即使包装成异步的也不会改变其阻塞属性,后续的操作都无法执行的。

所以你需要:

    // 标准库的TcpListener
    let listener = std::net::TcpListener::bind("0.0.0.0:3333")?;
    // 设置为非阻塞模式,让异步包装可以非阻塞轮询
    listener.set_nonblocking(true).unwrap();
    // 异步库的TcpListener
    let listener = TcpListener::from_std(listener)?;

如果你有注意到tokio::net::TcpListener::from_std文档说明的话,应该也是能看到以下内容:

    ...
    /// # Notes
    ///
    /// The caller is responsible for ensuring that the listener is in
    /// non-blocking mode. Otherwise all I/O operations on the listener
    /// will block the thread, which will cause unexpected behavior.
    /// Non-blocking mode can be set using [`set_nonblocking`].
    ...

所以在我拉下你的代码库的时候,即使是执行的cargo run [-r],在浏览器里我也是访问阻塞的

Lazy 2025-01-16 11:29

确实啊老铁仓库跟你贴出来的代码不一样,不过我看了一下,出现的问题都是一样的:

    // 标准库的TcpListener
    let listener = std::net::TcpListener::bind("0.0.0.0:3333")?;
    // 异步库的TcpListener
    let listener = TcpListener::from_std(listener)?;

你可能没注意到,标准库的TcpListener默认是阻塞的,所以服务的监听会直接阻塞线程,即使包装成异步的也不会改变其阻塞属性,后续的操作都无法执行的。

所以你需要:

    // 标准库的TcpListener
    let listener = std::net::TcpListener::bind("0.0.0.0:3333")?;
    // 设置为非阻塞模式,让异步包装可以非阻塞轮询
    listener.set_nonblocking(true).unwrap();
    // 异步库的TcpListener
    let listener = TcpListener::from_std(listener)?;

如果你有注意到tokio::net::TcpListener::from_std文档说明的话,应该也是能看到以下内容:

    ...
    /// # Notes
    ///
    /// The caller is responsible for ensuring that the listener is in
    /// non-blocking mode. Otherwise all I/O operations on the listener
    /// will block the thread, which will cause unexpected behavior.
    /// Non-blocking mode can be set using [`set_nonblocking`].
    ...

所以在我拉下你的代码库的时候,即使是执行的cargo run [-r],在浏览器里我也是访问阻塞的

Nayaka 2025-01-16 11:04

你这代码克隆下来的跟上面贴的都不一样

省流:"你在用右手指给自己右手背抓痒"

1 共 6 条评论, 1 页