Futures 0.3, async#await experience snapshot

This longish post is about my attempts to catch up to the latest Futures (0.3-alpha) and rust async#await, and prepare some existing Futures 0.1, Tokio 0.1, Hyper 0.12 projects, including a public crate, for an upgrade which presumably will offer tangible benefits, beyond just being necessary to keep current. I use “snapshot” in the title because I think this is very much an experience with the ecosystem at this moment in time.

Why now?

I’ve been interested since reading various early summaries in 2018, including more recently a Tokio blog post on the subject. For a while, I assumed I could just wait for Tokio and Hyper releases, or at least PRs/experimental branches that ported to the latest futures (as of this writing, futures-preview 0.3.0-alpha.16). However, more recent internals (IRLO) posts like this one and two, have given the impression that async#await is closer to being stabilized.

tokio-async-await

As tokio-async-await was longest on my radar, I started there. Apparently this was a just the wrong time. Tokio release 0.1.20, 2019-05-14 removed the async-await-preview feature, and after finding an awkward downgrade path, I find it doesn’t work on recent nightlies:

error[E0432]: unresolved import `std::await`
  --> /home/david/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-async-await-0.1.7/src/lib.rs:35:9
   |
35 | pub use std::await as std_await;
   |         ^^^^^^^^^^^^^^^^^^^^^^^ no `await` in the root

I decided this was wasting time with what is quickly becoming just a historic curiosity.

Futures 0.3 compatibility layer

A Futures 0.3 ⟷ 0.1 (bidirectional) compatibility layer was more recently released, 2019-4-15. In the last couple week I’ve used it to author my first async fn/block equivalents to the existing futures 0.1 combinators. In the below, I use the RFC described initialization pattern, to await an initial future, compose another future, await again, and finish with a prepare() step.

This composes a fully async fn resp_future_03:

Allocation overhead

I had previously gone through the exercise of removing all the Box allocation overhead from my futures 0.1 code. As of this latest conversion, two calls to boxed() have been reintroduced. I can only hope that this wrinkle will iron back out when Tokio and Hyper have full support for the same futures version. Stay tuned!

Syntax and formatting

Notice how the async keyword hasn’t drawn much controversy lately? :slight_smile:

I was previously vocal with concerns with and alternatives for the .await syntax and won’t repeat the same here. The formatting of the above source code is my best attempt, moving forward, to give a postfix unary .await operator sufficient visibility and distinction from field access. I am well aware that other developers and rustfmt are unlikely to unanimously agree with my formatting choices.

I will also be surprised if libraries/frameworks like Tokio don’t offer a macro version of this operation, wrapping .await, with additional functionality like recording mean wait time per call-site, or other logging. Given the chosen syntax, the only way to extend the await operation is via a macro. Between formatting alternatives and different macro names (it can’t be await!()), a likely outcome of the syntax decision will be additional confusion for new users, due to community divergence.

Implementing 0.3 Streams and Sinks

Arguably the more involved use of futures in body-image-futio are its two Stream and two Sink implementations. These traits have undergone significant changes in futures 0.3. A first 0.3 Stream implementation appears to be a fairly straightforward, methodical transformation:

I spent a good bit more time figuring out how to write complete futures 0.3 tests for this, as per the below. Firstly, the 0.3 Forward combinator no longer returns the moved Sink. Disappearing/reappearing Future output types has been a recurring theme in my 1.5 year Tokio/Futures evolution experience. After a lot of searching I eventually found PR #1441 partially explaining the change and alluding to a means to pass the Sink by reference. However, with my initial tests being purely with combinators and no async {} blocks, it wasn’t at all clear how to achieve this. I wasted hours trying various forms of Pin::new and pin_utils::pin_mut (undocumented) before arriving at the simpler form below. I suspect I still have quite a bit more to understand about Pin. StreamExt::forward could also use some better documentation regarding pass by reference. My documentation PR #1655. An example test with async block:

Porting the first Sink from 0.1 to 0.3 is much less straightforward. Due to changes in the 0.3 Sink interface, it is now necessary to signal any potential for Poll::Pending (in Sink context, lack of readiness to receive) in the trait method poll_ready, before the Item buffer is given. However, both of my Sink implementations need to know the byte length of the Item buffer, and furthermore, the result of conditionally calling tokio_threadpool::blocking, in order to correctly determine readiness. First draft, with an unacceptable panic:

For this I filed issue #1665, which includes an alternative proposal for support in the futures crate. With the research and prototyping for that, and a couple more days of local implementation and tests (~700 LoC), I now have what I think is mostly complete:

…up to the limit of what can be done before Tokio and Hyper are updated to futures 0.3. See also a design/verification Tokio issue #1147.

await keyword is not to be trifled with

An unexpected consequence, apparently of how await was made a keyword in the 2018 edition, is that it is a fatal error in 1.31.0 through 1.35.0 (at least) for a 2018 edition project to have it anywhere in the source code, even when it is only used in blocks which are not configured for inclusion‽ For example, the error at line 281 below is in a function with #[cfg(feature="futures_03")], and is fatal even when the non-default feature futures_03 is not enabled:

error[E0721]: `await` is a keyword in the 2018 edition
   --> body-image-futio/src/lib.rs:281:29
    |
281 |         let monolog = futr .await?;
    |                             ^^^^^ help: you can use a raw identifier to stay compatible: `r#await`

The comments and resolution of rust issue #53834 seems related. Any fix for this, would obviously first require agreeing that its a problem, and given rust release practices, is unlikely to be backported to a new 1.31.2 release, etc. So it won’t be possible for ecosystem lib crates (like mine) to release optional async functions and futures 0.3 support, along side current futures 0.1.x, for users who are prepared to use the latest nightly or stable rust. Instead it will require a breaking change MSRV bump, at least to 1.37.0, and a new release series. If Tokio, for example, were to continue to apply its current Supported Rust Versions policy, then such a release can’t be made until at least rust 1.40.0 is released. Of course, Tokio could relax its rules for alpha/beta releases in a new release series. Whom will bet production code on a Tokio alpha/beta?

Meta: user guide needed

One truly comprehensive (std, or 0.3, 1.0, or whatever it comes to be called) Futures and async#await user guide would be worth considerably more than the 100s of partial or outdated posts, threads, rustdocs, issues and PRs. Tracking all of the later down was at least half my effort to date. Until such a guide exists, here is a must-read and keep-open-browser-tabs list:

Conclusion

I’m looking forward to all this stabilizing in Rust, as well as new Tokio, Hyper, and probably several other major crate releases. When that will all happen is really anyone’s guess? My next step, once there are at least working experimental branches of Tokio and Hyper, would probably be to re-implement the Hyper client/server integration tests using async blocks instead of the combinator forms which were quite challenging to write the first time around:

Used by tests like this:

If there is interest in the topic, I’ll update here with developments as there is progress.

8 Likes

You can hide any unsupported syntax by putting it in a separate module:

#[cfg(feature="futures_03")]
mod impl_with_await;

Disabled modules aren’t parsed, so the compiler won’t see the .await there.

2 Likes

This feels a bit like a bug-that-becomes-a-feature, like public-in-private.

1 Like

It might be good to keep an eye out for the Rust Async Book.

It’s far from finished, but there has been activity in the repository over the last few days, so work is being picked up.