Hyper Traps

栏目: IT技术 · 发布时间: 5年前

内容简介:As a responsible Rust Evangelist, I try to propagate the language in my current job. So, once again, I’ve done a workshop, this time themed about async Rust. The goal was to write a small but non-trivial http service.As an aside, I tend to like the new wor

Hyper traps

As a responsible Rust Evangelist, I try to propagate the language in my current job. So, once again, I’ve done a workshop, this time themed about async Rust. The goal was to write a small but non-trivial http service.

As an aside, I tend to like the new working from home and most everything is fine. But doing hands-on workshops is definitely easier in person than over video-conferencing.

I didn’t want to dig into specific http frameworks, because then I wouldn’t be teaching so much Rust, but the frameworks instead. So we went with using hyper directly. In my experience, while a bit low-level, it’s a good-enough tool when you need to expose an API other stuff talks to over http, but don’t need any kind of HTML templating stuff. That’s not to say a framework won’t make your life easier if you do a lot of complex http-related stuff, we simply went with the smallest reasonable cannon that gets the job done.

And while the exercise went mostly well and both Rust and hyper are perceived in a tentatively-positive way, there were two traps that tripped my colleagues. I’d like to share them here ‒ there might be something that can be done about them and even if not, it might save someone from the same traps.

Note: I haven’t verified each of these snippets compiles ‒ there might be typos and such. They are illustrative.

Panics

While my stand on panics is that they are not supposed to happen and they are Rust’s bug-coping strategy, so they have no place at all in production application, they do happen during development.

When a panic happens in the response handler, hyper (or maybe tokio that sits below hyper ) recovers ‒ it saves the worker thread, it kills just that one task (connection) and the service can continue. Nevertheless, it does so in a very minimal way. The client connection is just abruptly cut off. One would like to have a 500-error response instead.

async fn handle(_: Request<Body>) -> Result<Response<Body>, Infallible> {
    unimplemented!()
}

#[tokio::main]
async fn main() {
    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));

    let make_svc = make_service_fn(|_conn| async {
        Ok::<_, Infallible>(service_fn(handle))
    });

    let server = Server::bind(&addr).serve(make_svc);

    if let Err(e) = server.await {
        error!("server error: {}", e);
    }
}

It makes sense for hyper not to do that, it’s a http library, not a framework. It shouldn’t have an opinion how your error page looks like, how much info it contains and such. So let’s do that ourselves. With a little bit digging, one finds the catch_unwind method in futures .

Few important things are:

  • One wants to use this method, not the catch_unwind from std . This is because futures are lazy and we want to catch the unwinds from its poll method, not from the future being created (which, if you use an async fn , probably can’t even panic).
  • Your future is likely not going to be unwind-safe. Honestly, unwind safety in Rust is a bit weird concept. Anyway, as we will let the task die the same way it does now, we just provide the handy 500-error response to the client. So we are going to declare it unwind safe and be done with it.
type HttpResult = Result<Response<Body>, Infallible>;

async fn handle(_: Request<Body>) -> HttpResult {
    unimplemented!()
}

async fn handle_panics(fut: impl Future<Output = HttpResult>)
    -> HttpResult
{
    // 1. Wrap the future in AssertUnwindSafe, to make the compiler happy
    //    and allow us doing this. The wrapper also implements `Future`
    //    and delegates `poll` inside.
    // 2. Turn panics falling out of the `poll` into errors. Note that we
    //    get `Result<Result<_, _>, _>` thing here.
    let wrapped = AssertUnwindSafe(fut).catch_unwind();
    match wrapped.await {
        // Here we unwrap just the outer panic-induced `Result`, returning
        // the inner `Result`
        Ok(response) => response,
        Err(_panic) => {
            error!("A panic happened. Run to the hills");
            let error = Response::builder()
                .status(StatusCode::INTERNAL_SERVER_ERROR)
                .body("We screwed up, sorry!".into())
                .unwrap();
            Ok(error)
        }
    }
}

#[tokio::main]
async fn main() {
    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));

    let make_svc = make_service_fn(|_conn| async {
        // Run `handle` immediately, which produces a Future (that's what
        // async functions do ‒ they return a Future right away, but don't
        // run that Future.). Feed that future to `handle_panics` to wrap
        // it up and return a panic-handling future.
        Ok::<_, Infallible>(service_fn(|req| handle_panics(handle(req))))
    });

    let server = Server::bind(&addr).serve(make_svc);

    if let Err(e) = server.await {
        error!("server error: {}", e);
    }
}

Ownership & async blocks

Let’s say the service grows a little bit in complexity and we want to extract the handling into a separate struct. Something like this:

struct Handler {
    // Interesting stuff goes in here
}

impl Handler {
    async fn handle(_: Request<Body>) -> HttpResult {
        unimplemented!()
    }
}

The Handler should effectively be a singleton ‒ it contains some shared information (maybe a handle to a database connection pool, or some global configuration). So we could place it into a global static like this:

static HANDLER: Lazy<Handler> = Lazy::new(|| Handler::create());

    // inside main:
    let make_svc = make_service_fn(|_conn| async {
        Ok::<_, Infallible>(service_fn(|req| HANDLER.handle(req)))
    });

Nevertheless, this feels ugly because of the global variable. It’s against common best practices, complicates testing, etc. So we would prefer to create one in main, wrap it in Arc and share between the handlers, something like this:

let handler = Arc::new(Handler::create());

let make_svc = make_service_fn(|_conn| async {
    let handler = Arc::clone(&handler);

    Ok::<_, Infallible>(service_fn(|req| async {
        let handler = Arc::clone(&handler);
        Ok::<_, Infallible>(router.route(req).await)
    }))
});

This will, however, start throwing variations of „cannot move out of“ and „does not live long enough“ lifetime errors. The usual tricks ‒ introducing more Arc::clone s and placing the move keyword at various places doesn’t help.

Now, the correct solution here is to move the Arc::clone outside of the async block. I’ll explain why that is in a while.

let handler = Arc::new(Handler::create());

let make_svc = make_service_fn(|_conn| {
    let handler = Arc::clone(&handler);

    async {
        Ok::<_, Infallible>(service_fn(move |req| {
            let handler = Arc::clone(&handler);

            async move {
                Ok::<_, Infallible>(router.route(req).await)
            }
        }))
    }
});

But for the explanation, we need to go a bit deeper.

Independent lifetimes

Because the created service handlers and the futures need to be independent in their lifetimes (we can’t predict how long each one will live, because we don’t know how long the clients will keep their connections open), they need to be 'static . That means one closure or future can’t just borrow from the parent context. Therefore, we need the move keywords at strategic positions.

We can’t leave the parent context „hollow“

So we know the innermost async block needs to move . But if we just do that, we consume the Arc from the closure. Yet, we can’t do that because the closure can potentially run multiple times ‒ creating multiple instances of the future. If the first one consumes the Arc , what would be left for the second one? (This is encoded in the closure being FnMut )

Therefore, we need to clone the Arc every time, so each future gets its own.

The async blocks are lazy

The async block doesn’t start running right away. It acts similar way to closures ‒ it just produces an anonymous struct that implements the Future trait. All the needed captured variables are stored in there, it’s shipped off and returned. It starts running only once the executor calls poll for he first time.

Therefore, cloning the Arc inside the block, like in the first example, is too late . By that time the block starts running, we have either moved (and consumed) the Arc from the closure’s context (and none is left for its second run) or we stored only a reference (and the parent closure might have been dropped by that time, creating a dangling reference), depending on if we use async move or async only.

Therefore, we first have to clone our Arc and only then build the future. That leads to having half of the closure body be synchronous/blocking, the other half async .

Solution: async closures

As you can guess, this is not something one figures out right away, especially not as a Rust novice just starting to grasp the ownership rules. The good thing is the compiler won’t let faulty code compile, but it’s still not very intuitive and friendly. I myself needed several rounds of negotiation with the borrow checker to find a solution even though I know the low-level principles what hides behind an async block. There might be simpler solutions possible.

The proper solution here is probably to use async closures , because then the code would have only 2 levels, not 4 and become much more obvious.

let make_svc = make_service_fn(async move |_conn| {
    let handler = Arc::clone(&handler);

    Ok::<_, Infallible>(service_fn(async move |req| {
        let handler = Arc::clone(&handler);
        Ok::<_, Infallible>(router.route(req).await)
    }))
});

But this is not stabilized yet (and I’m not sure if it’ll work). So until then, we are probably left with documenting this pitfall in a well visible place.

Closing thoughts

I don’t want to be negative ‒ it’s been a long way from how the hyper-0.12 code looked like. It’s almost pleasant to use. But it seems there are some more rough edges to polish.


以上所述就是小编给大家介绍的《Hyper Traps》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Web全栈工程师的自我修养

Web全栈工程师的自我修养

余果 / 人民邮电出版社 / 2015-9-1 / 49.00

全栈工程师正成为 IT 行业的新秀,论是上市互联网公司还是创业公司,都对全栈工程师青睐有加。本书作者是腾讯公司高级工程师,在前端、后端和APP开发方面都有丰富的经验,在本书中分享了全栈工程师的技能要求、核心竞争力、未来发展方向、对移动端的思考。除此之外,本书还详细记录了作者从零开始、学习成长的心路历程。 本书内容全面,客观务实,适合互联网行业新人、程序员,以及期待技术转型的从业者阅读参考。一起来看看 《Web全栈工程师的自我修养》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

MD5 加密
MD5 加密

MD5 加密工具

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器