# some_executor

Rust made the terrible mistake of not having an async executor in std. Worse, there is no
trait for executors to implement, and no API for async code to expect. So everyone writes
their code against one specific executor, and it's always tokio. But tokio has too many
drawbacks to be the universal choice, and the other executors are too cumbersome to be
practical. Async rust is stuck in limbo.
There are many proposals to fix this. This one's mine.
`some_executor` is a small crate that sits between the code that *has* futures and the code
that *runs* them:
* **If you spawn futures**, you get one obvious `spawn` that works on "some" executor: a
generic argument, a stored trait object, the executor your caller is already running on,
or a program-wide global. Whichever you pick, you get back an observer you can await,
poll, detach, or drop to cancel.
* **If you write an executor**, you implement one trait, and cancellation, task-locals,
observers, priorities and hints are done for you. You get to focus on scheduling.
* **If you write async code**, you get the featureset that (in my opinion) is table stakes for
async rust: cancellation, task-locals, priorities, execution hints, task IDs and labels.
They work the same on every executor, because the executor doesn't implement them; this
crate does.
There is also a built-in fallback executor, so all of this works out of the box. It is not
good, but it is always there; when you want a real one, see the reference executors below.
# Quick start
```
use some_executor::SomeExecutor;
use some_executor::current_executor::current_executor;
use some_executor::observer::FinishedObservation;
use some_executor::task::{Configuration, Task};
# // Runs on the event loop on wasm32 and blocks natively. It cannot be a plain
# // block_on: on the browser main thread that would starve the event loop the
# // fallback executor needs to start its worker. See "Crossing from sync into async".
# wasm_lite_std::async_doctest!(async {
// A Task is a future plus a label and some scheduling metadata.
let task = Task::without_notifications(
"add".to_string(),
Configuration::default(),
async { 2 + 2 },
);
// Spawn it on whatever executor is current: the one this task is running on,
// else the thread's, else the global one, else the built-in fallback.
let mut executor = current_executor();
let observer = executor.spawn(task);
// The observer is a Future. Dropping it instead would cancel the task.
match observer.await {
FinishedObservation::Ready(value) => assert_eq!(value, 4),
FinishedObservation::Cancelled => unreachable!(),
}
# });
```
The rest of this page is organized by what you are trying to do.
# Spawning tasks
Every path to spawning starts with a `Task`: your future, a `String` label, and a
`Configuration`. Then pick how you want to get hold of an executor.
| Take an executor as a generic argument and monomorphize | `SomeExecutorExt` (or `StaticExecutorExt`, `LocalExecutorExt`) |
| Store an executor in a struct, erasing its type | `DynExecutor`, `DynStaticExecutor`, `SomeLocalExecutor` |
| Borrow the executor your caller is already running on | `current_executor`, or `Task::spawn_current` for fire-and-forget |
| Use the executor pinned to this thread | `thread_executor`, `thread_static_executor`, `thread_local_executor` |
| Spawn from nowhere in particular (a signal handler, say) | `global_executor` |
Whichever you use, `spawn` returns an `Observer` (usually a `TypedObserver`):
* `.await` it to get a `FinishedObservation`: the task's output, or `Cancelled`.
* Call [`observe`](Observer::observe) to peek without waiting.
* Call [`detach`](Observer::detach) to let the task run to completion unobserved.
* Drop it to request cancellation. Cancellation is cooperative: the task sees it through
`IS_CANCELLED`, and executors may stop polling.
## Which executor is "current"?
`current_executor` walks a fixed hierarchy and always returns something:
1. The executor the current task was spawned on (`TASK_EXECUTOR`).
2. The executor set for this thread with `set_thread_executor`.
3. The program-wide executor set with `set_global_executor`.
4. The built-in fallback executor.
The fallback executor exists so that libraries built on this crate work with zero
configuration. It prints a warning when used, because it is not production quality; install
a real one (see [reference executors](#reference-executors)) with `set_global_executor` or
`set_thread_executor`. Set `SOME_EXECUTOR_BUILTIN_SHOULD_PANIC=1` to make the fallback
panic instead, which is a good way to find places you forgot to do that.
## Three flavors of executor
Executors differ in what futures they can accept. This crate models the three cases that
come up in practice, each with a generic (`*Ext`) trait for static dispatch and an object-safe
base trait for dynamic dispatch:
| `Send + 'static` | `SomeExecutor` (see `DynExecutor`) | `SomeExecutorExt` | Thread pools; anything that moves work |
| `'static`, not `Send` | `SomeStaticExecutor` (see `DynStaticExecutor`) | `StaticExecutorExt` | Main-thread and single-threaded executors; wasm |
| Neither (`'a` and `!Send`) | `SomeLocalExecutor` | `LocalExecutorExt` | Executors scoped to a stack frame |
The `Send` and static flavors are cloneable, like a channel sender, and can be discovered
through `current_executor` and friends. Local executors are borrowed and cannot be
cloned; the `SomeLocalExecutor` docs explain the lifetime parameter and why. If you
don't know which you need, start with `SomeExecutor`.
## Configuring a task
`Configuration` (build one with `ConfigurationBuilder`) carries three things an
executor may use, and none of them changes what your future does:
* A `Hint`: is this task expected to be I/O-bound, CPU-bound, or unknown?
* A `Priority`, for executors that schedule by priority.
* A `poll_after` `Instant`, before which the executor must not poll the task.
Every task also has a label and a `TaskID`, both visible from inside the task and from its
observer, which makes tracing and logging across executors practical.
## Reference executors
These crates implement the traits above and are the ones I actually use:
* [some_global_executor](https://sealedabstract.com/code/some_global_executor) is a
thread-pool executor for `Send` tasks. It runs on OS threads natively and on web workers
on wasm32, and can install itself as the thread or global executor. If you want one
executor to replace the built-in fallback, this is it.
* [some_local_executor](https://sealedabstract.com/code/some_local_executor) is a local
executor that runs its tasks on the current thread and can also receive tasks from other
threads.
* [test_executors](https://sealedabstract.com/code/test_executors) provides toy executors good
enough for unit tests.
# Crossing from sync into async
Every program crosses from sync into async exactly once: in `fn main`, in a test, in a CLI
tool, at an FFI callback. Which API you want depends on whether you are *choosing* an
executor or *using* one.
Choosing is `ExecutorMain`. It is the trait spelling of "construct the chosen backend,
install it, run this future", so that an `#[some_executor::main(SomeBackend)]` attribute can
expand to something that compiles against a backend the macro has never heard of. It returns
`()` rather than the future's output, which is what makes it implementable on the wasm32 main
thread as well as natively.
Using is `SomeExecutor::block_on`. Given an executor -- including the one
`current_executor` hands you -- it drives a future to completion and returns its output,
blocking the calling thread:
```
use some_executor::SomeExecutor;
use some_executor::current_executor::current_executor;
# // A doctest runs on the browser main thread, which cannot block; on wasm32 this
# // runs the body on a worker, where it can. Natively it just calls the closure.
# wasm_lite_std::worker_doctest!(|| {
let mut executor = current_executor();
assert_eq!(executor.block_on(async { 2 + 2 }), 4);
# });
```
Underneath both is the free [`block_on`](fn@block_on) function, which polls a future in place
on the calling thread with no executor involved. Because the future never leaves the thread,
it needs neither `Send` nor `'static` and may borrow from the stack -- unlike spawning:
```
# wasm_lite_std::worker_doctest!(|| {
let name = String::from("world");
let greeting = some_executor::block_on(async { format!("hello {}", name.as_str()) });
assert_eq!(greeting, "hello world");
# });
```
Blocking is not universally available, which is why the entry point and the primitive are
separate. It works whenever the blocking thread can keep driving every scheduler the future
depends on: another thread is doing the work, or the executor owns its own loop and runs it
here. It cannot work when the wakeups come from a scheduler this thread can only run by
unwinding -- the browser main thread, where the JavaScript event loop delivers every timer,
promise and worker message. [`block_on`](fn@block_on) panics there with an explanation
rather than hanging the tab; a wasm32 worker has no such problem, and `ExecutorMain` is the
portable choice for an entry point.
# Writing async code
Mostly, write the code you want to write. Nothing here requires you to know which executor
you are running on. What you get on top:
* `task_local!` declares task-local storage, comparable to `thread_local!` or tokio's
`task_local!`, with `scope`, `get`, `set` and immutable (`static const`) variants.
* Built-in task-locals describe the current task: `TASK_ID`, `TASK_LABEL`,
`TASK_PRIORITY`, and `IS_CANCELLED`.
* `IS_CANCELLED` lets long-running work notice a cancellation request and return early.
* `TASK_EXECUTOR` and `TASK_STATIC_EXECUTOR` hold the executor a task was spawned on, which
is how `current_executor` and `Task::spawn_current` find it.
```
use some_executor::task_local;
use some_executor::task::{TASK_LABEL, IS_CANCELLED};
task_local! {
static REQUEST_ID: u64;
}
async fn handle() {
let label = TASK_LABEL.with(|l| l.cloned());
let request = REQUEST_ID.get();
// Do a unit of work, then check for cancellation before the next one.
if IS_CANCELLED.with(|c| c.map(|c| c.is_cancelled()).unwrap_or(false)) {
return;
}
let _ = (label, request);
}
```
# Implementing an executor
An executor is anything that accepts a `Task` and polls it. In outline:
1. Implement `SomeExecutor` (for `Send` futures), `SomeStaticExecutor` (for `'static`,
`!Send` futures) and/or `SomeLocalExecutor` (for borrowed futures). Add the matching
`*Ext` marker trait if your executor is `Clone`.
2. In your `spawn`, call `Task::spawn` (or `spawn_static` / `spawn_local`) with `&mut self`.
You get back a `SpawnedTask` to schedule and an observer to hand to the caller.
3. Poll the spawned task. Its `poll` takes an executor context so that
`current_executor` works inside the task; the spawned task itself installs task-locals,
reports completion to the observer, and stops early on cancellation.
4. Respect `poll_after`: do not poll before that instant. Sleep, defer, re-queue,
whatever fits your design. This is the main gotcha.
5. Optionally implement `ExecutorNotified` to be told when a task's observer requests
cancellation, so you can drop it early instead of discovering that on the next poll.
6. Optionally register yourself with `set_thread_executor` / `set_global_executor` (or the
static and local equivalents) so that `current_executor` finds you.
7. If your executor runs tasks on the calling thread, override
`SomeExecutor::block_on_objsafe`. The default parks the caller, which is correct for a
thread pool and a deadlock for a current-thread executor; run your own polling loop until
the future resolves instead. Implement `ExecutorMain` too, so `#[some_executor::main]`
can name you.
For static executors, `static_support` provides `OwnedSomeStaticExecutorErasingNotifier`
to erase your notifier type into the common `DynStaticExecutor` shape. For the object-safe
methods (`spawn_objsafe` and friends), the `ObjSafe*` and `Boxed*` type aliases at the crate
root spell out the erased types so you don't have to.
# Compared with `executor-trait`
One way to understand this crate is as an alternative to
[executor-trait](https://crates.io/crates/executor-trait/). I like it a lot; here is why I
made this instead:
1. To support futures whose output isn't `()`.
2. To avoid boxing futures where it isn't necessary.
3. To carry hints and priorities to the executor.
4. To provide task-locals and the other features async code actually needs.
5. To support cancellation much more robustly.
Philosophically, `executor-trait` ships the lowest common denominator that every executor can
support. This crate ships the **highest common denominator that all async code can use**,
together with **polyfills and fallbacks so every executor can offer it**, even ones that don't
support a feature natively. It is straightforward to implement either crate's API in terms
of the other, so the two can be used together.
# wasm32
`wasm32-unknown-unknown` is a first-class target, with and without atomics. Timing uses
`Instant`, which is `std::time::Instant` natively and a web-clock on wasm, and the fallback
executor schedules through the browser event loop.
# Status
This interface is unstable and may change.