Description
openedon Feb 12, 2018
When creating a new Executor from scratch, there's a lot of subtle behavior which currently isn't documented.
This can create edge cases where one Executor behaves differently from another Executor, and Futures might start to rely upon the behavior of a particular Executor.
I believe these behaviors should be documented:
-
If a Future calls
task.notify()
multiple times within a certain time frame, it's allowed for the Executor to only callpoll
once (rather than once pertask.notify()
)Of course an Executor must call
poll
aftertask.notify()
, but it is allowed to combine multiple calls totask.notify()
together. As an example:let task = futures::task::current(); task.notify(); task.notify();
With the above code, the Executor is allowed to call
poll
only once, even thoughtask.notify()
was called twice. -
Under certain pathological situations, it is allowed for the Executor to deadlock.
For example, a Future that calls
task.notify()
inside ofpoll
can sometimes trigger an infinite loop:struct MyFuture; impl Future for MyFuture { type Item = (); type Error = (); fn poll(&mut self) -> futures::Poll<Self::Item, Self::Error> { let task = futures::task::current(); task.notify(); Ok(futures::Async::NotReady) } }
In the above example, it is allowed for the Executor to deadlock, go into a 100% CPU infinite loop, etc.
In non-pathological situations, it is allowed for a Future to call
task.notify()
inside ofpoll
, and in that situation the Executor must not deadlock. -
Futures aren't supposed to call
task.notify()
if they cannot make any progress.This is related to the above point. If a Future calls
task.notify()
when nothing will change, then there is the possibility of a deadlock, so they shouldn't do that.This isn't a contract for Executors, it's a contract for Futures, but it affects the way that Executors are allowed to be implemented.
In particular, if a Future calls
task.notify()
without making any progress, then the behavior of the Executor is undefined and it can do anything it wants, including deadlock. -
Executors are allowed to synchronously call thepoll
method immediately whentask.notify()
is called. In other words, it isn't guaranteed thattask.notify()
is always asynchronous.This affects any situation where a Future callstask.notify()
and then proceeds to do something afterwards. The Future has to deal with the possibility that when it callstask.notify()
it might immediately have consequences.A contrived example is sharing aRefCell
between two Futures. If Future A mutably borrows theRefCell
, and then it callstask.notify()
which triggers Future B, and thepoll
implementation of Future B also mutably borrows theRefCell
, then it might trigger a panic.This behavior is allowed because the Executor is allowed to call thepoll
method immediately whentask.notify()
is called, so Futures need to plan for that. -
When calling
task.notify()
, it must not callpoll
immediately. In other words,task.notify()
must always be delayed, it yields to the event loop.