-
Notifications
You must be signed in to change notification settings - Fork 90
Add 'stream' and 'future' types #405
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
22 commits
Select commit
Hold shift + click to select a range
7d1034a
Add 'stream' and 'future' types
lukewagner e8198f7
Grammar fix
lukewagner e6469e8
Spelling fix
lukewagner 15298d5
Clarify wording in Async.md concerning the writable end
lukewagner fa3cd3a
Mention the callback option alongside task.wait
lukewagner a864991
Add <typeidx> to {stream,future}.{read,write}
lukewagner 0b5247f
Handle the concurrently-closed case in {stream,future}.cancel-{read,w…
lukewagner b20409a
Add note on spec-internal state vs. implementation
lukewagner a15ec3f
Update channel/pipe wording
lukewagner 4e456d7
Improve wording
lukewagner 08b1387
Put the canonopts on {stream,future}.{read,write} instead of copying …
lukewagner 87f7b85
Allow sync task.{wait,yield,poll} and {stream,future}.{read,write}
lukewagner 300c86c
Only enforce scoping for streams/futures containing borrows
lukewagner 70d727a
Break waitable.drop into subtask.drop and {stream,future}.close-{read…
lukewagner 3cf3d5f
Remove dangling syntax rule for waitable.drop
lukewagner 4581ba5
Update subsection links and other dangling waitable.drop reference
lukewagner e074f41
Add 'error' type and 'canon error.{new,debug-message,drop}' built-ins
lukewagner 30061e5
Update {stream,future}.close-writable descriptions
lukewagner 4306ee2
Add example to explainer text about 'error'
lukewagner fcea885
Remove restriction on write-before-lift, remove invalid assert, add test
lukewagner f9b341b
Add <typeidx> to {stream,future}.cancel-{read,write}
lukewagner e8d192e
Rename 'error' to 'error-context'
lukewagner File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,6 +17,7 @@ summary of the motivation and animated sketch of the design in action. | |
* [Current task](#current-task) | ||
* [Subtask and Supertask](#subtask-and-supertask) | ||
* [Structured concurrency](#structured-concurrency) | ||
* [Streams and Futures](#streams-and-futures) | ||
* [Waiting](#waiting) | ||
* [Backpressure](#backpressure) | ||
* [Returning](#returning) | ||
|
@@ -106,8 +107,30 @@ Thus, backpressure combined with the partitioning of low-level state provided | |
by the Component Model enables sync and async code to interoperate while | ||
preserving the expectations of both. | ||
|
||
[TODO](#todo): `future` and `stream` types that can be used in function | ||
signatures will be added next. | ||
In addition to being able to define and call whole functions asynchronously, | ||
the `stream` and `future` types can be used in function signatures to pass | ||
parameters and results incrementally over time, achieving finer-grained | ||
concurrency. Streams and futures are thus not defined to be free-standing | ||
resources with their own internal memory buffers (like a traditional channel or | ||
pipe) but, rather, more-primitive control-flow mechanisms that synchronize the | ||
incremental passing of parameters and results during cross-component calls. | ||
Higher-level resources like channels and pipes could then be defined in terms | ||
of these lower-level `stream` and `future` primitives, e.g.: | ||
```wit | ||
resource pipe { | ||
constructor(buffer-size: u32); | ||
write: func(bytes: stream<u8>) -> result; | ||
read: func() -> stream<u8>; | ||
} | ||
``` | ||
but also many other domain-specific concurrent resources like WASI HTTP request | ||
and response bodies or WASI blobs. Streams and futures are however high-level | ||
enough to be bound automatically to many source languages' built-in concurrency | ||
features like futures, promises, streams, generators and iterators, unlike | ||
lower-level concurrency primitives (like callbacks or `wasi:[email protected]` | ||
`pollable`s). Thus, the Component Model seeks to provide the lowest-level | ||
fine-grained concurrency primitives that are high-level and idiomatic enough to | ||
enable automatic generation of usable language-integrated bindings. | ||
|
||
|
||
## Concepts | ||
|
@@ -180,18 +203,80 @@ invocation of an export by the host. Moreover, at any one point in time, the | |
set of tasks active in a linked component graph form a forest of async call | ||
trees which e.g., can be visualized using a traditional flamegraph. | ||
|
||
The Canonical ABI's Python code enforces Structured Concurrency by maintaining | ||
a simple per-[`Task`] `num_async_subtasks` counter that traps if not zero when | ||
the `Task` finishes. | ||
The Canonical ABI's Python code enforces Structured Concurrency by incrementing | ||
a per-[`Task`] counter when a `Subtask` is created, decrementing when a | ||
`Subtask` is destroyed, and trapping if the counter is not zero when the `Task` | ||
attempts to exit. | ||
|
||
### Streams and Futures | ||
|
||
Streams and Futures have two "ends": a *readable end* and *writable end*. When | ||
*consuming* a `stream` or `future` value as a parameter (of an export call with | ||
a `stream` or `future` somewhere in the parameter types) or result (of an | ||
import call with a `stream` or `future` somewhere in the result type), the | ||
sunfishcode marked this conversation as resolved.
Show resolved
Hide resolved
|
||
receiver always gets *unique ownership* of the *readable end* of the `stream` | ||
or `future`. When *producing* a `stream` or `future` value as a parameter (of | ||
an import call) or result (of an export call), the producer can either | ||
*transfer ownership* of a readable end it has already received or it can create | ||
a fresh writable end (via `stream.new` or `future.new`) and then lift this | ||
writable end to create a fresh readable end in the consumer while maintaining | ||
ownership of the writable end in the producer. To maintain the invariant that | ||
readable ends are unique, a writable end can be lifted at most once, trapping | ||
otherwise. | ||
|
||
Based on this, `stream<T>` and `future<T>` values can be passed between | ||
functions as if they were synchronous `list<T>` and `T` values, resp. For | ||
example, given `f` and `g` with types: | ||
```wit | ||
f: func(x: whatever) -> stream<T>; | ||
g: func(s: stream<T>) -> stuff; | ||
``` | ||
`g(f(x))` works as you might hope, concurrently streaming `x` into `f` which | ||
concurrently streams its results into `g`. If `f` has an error, it can close | ||
its returned `stream<T>` with an [`error-context`](Explainer.md#error-context-type) | ||
value which `g` will receive along with the notification that its readable | ||
stream was closed. | ||
|
||
If a component instance *would* receive the readable end of a stream for which | ||
it already owns the writable end, the readable end disappears and the existing | ||
writable end is received instead (since the guest can now handle the whole | ||
stream more efficiently wholly from within guest code). E.g., if the same | ||
component instance defined `f` and `g` above, the composition `g(f(x))` would | ||
just instruct the guest to stream directly from `f` into `g` without crossing a | ||
component boundary or performing any extra copies. Thus, strengthening the | ||
previously-mentioned invariant, the readable and writable ends of a stream are | ||
unique *and never in the same component*. | ||
|
||
Given the readable or writable end of a stream, core wasm code can call the | ||
imported `stream.read` or `stream.write` canonical built-ins, resp., passing the | ||
pointer and length of a linear-memory buffer to write-into or read-from, resp. | ||
These built-ins can either return immediately if >0 elements were able to be | ||
written or read immediately (without blocking) or return a sentinel "blocked" | ||
value indicating that the read or write will execute concurrently. The | ||
readable and writable ends of streams and futures each have a well-defined | ||
parent `Task` that will receive "progress" events on all child streams/futures | ||
that have previously blocked. | ||
|
||
From a [structured-concurrency](#structured-concurrency) perspective, the | ||
readable and writable ends of streams and futures are leaves of the async call | ||
tree. Unlike subtasks, the parent of the readable ends of streams and future | ||
*can* change over time (when transferred via function call, as mentioned | ||
above). However, there is always *some* parent `Task` and this parent `Task` | ||
is prevented from orphaning its children using the same reference-counting | ||
guard mentioned above for subtasks. | ||
|
||
### Waiting | ||
|
||
When a component asynchronously lowers an import, it is explicitly requesting | ||
that, if the import blocks, control flow be returned back to the calling task | ||
so that it can do something else. Eventually though a task may run out of other | ||
so that it can do something else. Similarly, if `stream.read` or `stream.write` | ||
would block, they return a "blocked" code so that the caller can continue to | ||
make progress on other things. But eventually, a task will run out of other | ||
things to do and will need to **wait** for progress on one of the task's | ||
subtasks. While a task is waiting, the runtime can switch to other running | ||
tasks or start new tasks by invoking exports. | ||
subtasks, readable stream ends, writable stream ends, readable future ends or | ||
writable future ends, which are collectively called its **waitables**. While a | ||
task is waiting on its waitables, the Component Model runtime can switch to | ||
other running tasks or start new tasks by invoking exports. | ||
dicej marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
The Canonical ABI provides two ways for a task to wait: | ||
* The task can call the [`task.wait`] built-in to synchronously wait for | ||
|
@@ -234,13 +319,23 @@ the "started" state. | |
|
||
### Returning | ||
|
||
The way an async Core WebAssembly function returns its value is by calling | ||
[`task.return`], passing the core values that are to be lifted. | ||
|
||
The main reason to have `task.return` is so that a task can continue execution | ||
after returning its value. This is useful for various finalization tasks (such | ||
as logging, billing or metrics) that don't need to be on the critical path of | ||
returning a value to the caller. | ||
The way an async function returns its value is by calling [`task.return`], | ||
passing the core values that are to be lifted as *parameters*. Additionally, | ||
when the `always-task-return` `canonopt` is set, synchronous functions also | ||
return their values by calling `task.return` (as a more expressive and | ||
general alternative to `post-return`). | ||
|
||
Returning values by calling `task.return` allows a task to continue executing | ||
even after it has passed its initial results to the caller. This can be useful | ||
for various finalization tasks (freeing memory or performing logging, billing | ||
or metrics operations) that don't need to be on the critical path of returning | ||
a value to the caller, but the major use of executing code after `task.return` | ||
is to continue to read and write from streams and futures. For example, a | ||
stream transformer function of type `func(in: stream<T>) -> stream<U>` will | ||
immediately `task.return` a stream created via `stream.new` and then sit in a | ||
loop interleaving `stream.read`s (of the readable end passed for `in`) and | ||
`stream.write`s (of the writable end it `stream.new`ed) before exiting the | ||
task. | ||
|
||
A task may not call `task.return` unless it is in the "started" state. Once | ||
`task.return` is called, the task is in the "returned" state. A task can only | ||
|
@@ -419,21 +514,26 @@ For now, this remains a [TODO](#todo) and validation will reject `async`-lifted | |
|
||
## TODO | ||
|
||
Native async support is being proposed in progressive chunks. The following | ||
features will be added in future chunks to complete "async" in Preview 3: | ||
* `future`/`stream`/`error`: add for use in function types for finer-grained | ||
concurrency | ||
* `subtask.cancel`: allow a supertask to signal to a subtask that its result is | ||
no longer wanted and to please wrap it up promptly | ||
* allow "tail-calling" a subtask so that the current wasm instance can be torn | ||
down eagerly | ||
* `task.index`+`task.wake`: allow tasks in the same instance to wait on and | ||
wake each other (async condvar-style) | ||
Native async support is being proposed incrementally. The following features | ||
will be added in future chunks roughly in the order list to complete the full | ||
"async" story, with a TBD cutoff between what's in [WASI Preview 3] and what | ||
comes after: | ||
* `nonblocking` function type attribute: allow a function to declare in its | ||
type that it will not transitively do anything blocking | ||
* define what `async` means for `start` functions (top-level await + background | ||
tasks), along with cross-task coordination built-ins | ||
* `subtask.cancel`: allow a supertask to signal to a subtask that its result is | ||
no longer wanted and to please wrap it up promptly | ||
* zero-copy forwarding/splicing and built-in way to "tail-call" a subtask so | ||
that the current wasm instance can be torn down eagerly while preserving | ||
structured concurrency | ||
* some way to say "no more elements are coming for a while" | ||
* `recursive` function type attribute: allow a function to be reentered | ||
recursively (instead of trapping) | ||
* enable `async` `start` functions | ||
recursively (instead of trapping) and link inner and outer activations | ||
* add `stringstream` specialization of `stream<char>` (just like `string` is | ||
a specialization of `list<char>`) | ||
* allow pipelining multiple `stream.read`/`write` calls | ||
* allow chaining multiple async calls together ("promise pipelining") | ||
* integrate with `shared`: define how to lift and lower functions `async` *and* | ||
`shared` | ||
|
||
|
@@ -475,3 +575,5 @@ features will be added in future chunks to complete "async" in Preview 3: | |
[stack-switching]: https://github.com/WebAssembly/stack-switching/ | ||
[JSPI]: https://github.com/WebAssembly/js-promise-integration/ | ||
[shared-everything-threads]: https://github.com/webAssembly/shared-everything-threads | ||
|
||
[WASI Preview 3]: https://github.com/WebAssembly/WASI/tree/main/wasip2#looking-forward-to-preview-3 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.