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.
https://github.com/dekellum/body-image/blob/31659635/body-image-futio/src/lib.rs#L280
This composes a fully async fn resp_future_03
:
https://github.com/dekellum/body-image/blob/31659635/body-image-futio/src/lib.rs#L375
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?
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 Stream
s and Sink
s
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:
https://github.com/dekellum/body-image/blob/31659635/body-image-futio/src/uni_image.rs#L272
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
:
https://github.com/dekellum/body-image/blob/31659635/body-image-futio/src/uni_sink.rs#L134
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:
https://github.com/dekellum/body-image/blob/244ece05/body-image-futio/src/uni_sink.rs#L147
…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:
-
RFC 2394: async#await (still very relevant!)
-
rust nightly std::future, std::task, std::pin,
-
futures 0.3 API rustdoc (custom hosted since docs.rs has various problems with futures-preview-* alpha crates)
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:
https://github.com/dekellum/body-image/blob/244ece05/body-image-futio/src/futio_tests/server.rs#L304
Used by tests like this:
https://github.com/dekellum/body-image/blob/244ece05/body-image-futio/src/futio_tests/server.rs#L200
If there is interest in the topic, I'll update here with developments as there is progress.