Skip to content

Better documentation for how to create an Executor #754

Closed

Description

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 call poll once (rather than once per task.notify())

    Of course an Executor must call poll after task.notify(), but it is allowed to combine multiple calls to task.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 though task.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 of poll 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 of poll, 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 the poll method immediately when task.notify() is called. In other words, it isn't guaranteed that task.notify() is always asynchronous.

    This affects any situation where a Future calls task.notify() and then proceeds to do something afterwards. The Future has to deal with the possibility that when it calls task.notify() it might immediately have consequences.

    A contrived example is sharing a RefCell between two Futures. If Future A mutably borrows the RefCell, and then it calls task.notify() which triggers Future B, and the poll implementation of Future B also mutably borrows the RefCell, then it might trigger a panic.

    This behavior is allowed because the Executor is allowed to call the poll method immediately when task.notify() is called, so Futures need to plan for that.

  • When calling task.notify(), it must not call poll immediately. In other words, task.notify() must always be delayed, it yields to the event loop.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions