So, I'm going to start quite far back to describe exactly why tokio-async-await
and futures(0.3)::compat
are incompatible.
First of all, the design of both futures(0.1)::Future
and std::future::Future
are based on a scheme where you spawn a future as a "task" onto an "executor" to run it. The executor will poll the future once then park it until it gets a notification that the task is ready to run again. Running the future and routing the notification from the underlying async IO provided by the OS are completely decoupled (the latter is commonly called the "reactor", in Tokio's case they allow you to run both on the same thread for better performance).
With futures(0.1)
this notification is handled by futures(0.1)::task::Task
, the executor ensures that Task::current()
will magically (via thread local storage) return a handle that can be used to notify it about readiness of the current task (IMO this naming is confusing because the Task
is not the task, it is just a notification handle for the task, I will consistently use code
formatting to distinguish them).
With std::futures::Future
the notification is handled by std::task::Waker
, the executor will pass a reference to a waker for the current task into the future as part of the std::task::Context
.
Now, tokio-async-await
has one purpose: to allow you to write async fn
and async {}
using Tokio's IO running on Tokio's executor. Because Task::current()
is magic and will implicitly propagate through any intermediate std::future::Future
it can ignore the std::task::Waker
and just inject a dummy that doesn't work. Once you get down to an IO future that actually needs to register for notifications with the reactor it can just call Task::current()
and acquire the handle that the executor stashed away earlier.
With futures(0.3)::compat
the aim is to have a fully general bi-directional compatibility layer, allowing you to run std::future::Future
on a futures(0.1)
based executor, or run futures(0.1)::Future
on a std::task
based executor, or poll either as part of a larger future constructed from either types. To make this work in all cases it must translate between these two waker systems, when you use .compat()
to convert a std::future::Future
into a futures(0.1)::Future
we add a layer that will call Task::current()
to acquire the current notification handle and then wrap that into a type that implements std::task::Waker
and pass this into the underlying std::future::Future
, so if the underlying future attempts to call cx.waker().wake()
this will then call task.notify()
so that the futures(0.1)
executor will see it. Similarly converting a futures(0.1)::Future
into a std::future::Future
will add a layer that takes the waker off the context and reconfigures Task::current()
to return a Task
that will call waker.wake()
when task.notify()
is called.
One consequence of these compatibility layers is that when you use futures(0.3)::compat
to run a futures(0.1)
based IO future on a futures(0.1)
based executor with a middle layer of async fn
we actually use both notification systems, the underlying IO future will call task.notify()
, this goes into the Task
that the 0.1 -> std
compat layer setup which calls waker.wake()
, which in turn goes into the Waker
added by the std -> 0.1
compat layer which calls task.notify()
on the Task
setup by the executor.
The incompatibility comes from tokio-async-await
assuming that Task::current
will always return the Task
setup by the executor. Because futures(0.3)::compat
overrides this with a Task
that proxies through to the Waker
it was provided it breaks this underlying assumption.
You should be able to do something very close to your old implementation, just using combinators to convert the std::future::Future
that async move
gives you to a futures(0.1)::Future
.
tokio::spawn(async move {
let response = await!(handle_request(pool)).unwrap();
println!("{}", response);
}.unit_error().boxed().compat());