Skip to content
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

Async: add and decouple "waitable sets" from tasks #438

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open

Conversation

lukewagner
Copy link
Member

Currently, every task has an implicit set of "waitables" (async import calls, async stream/future operations) that are collectively waited on by task.wait. However, for more advanced cases, different libraries may each need to own and manage their own sets of waitables independently of tasks. An example would a component that uses libc (which will need to maintain a set of waitables to implement select()) and contains a language runtime which implements its own async runtime that is not based on libc. This PR decouples waitable sets from tasks, making them first-class with new built-ins to create, use and destroy them.

The PR is broken into two initial commits:

The first commit is pure refactoring, no semantic changes. The first commit introduces the Waitable and WaitableSet classes and uses them to re-implement the current async functionality which makes the next commit much smaller. This ended up enabling a bunch of simplifications to the Python code that touches a bunch of code and descriptive text in CanonicalABI.md so it's a big commit and I'd recommend reading it separately from the next one (or skipping if you just want a summary of the semantic changes). Some interesting changes in this refactoring are:

  • "events" are no longer stored in a separate queue between when the event is generated and consumed by core wasm. Instead, events are stored as a field in the waitable element of the waitables table until they are delivered to core wasm.
  • The readable/writable ends of streams/futures are no longer called *Handles, but *Ends. So "handles" are for resources, "ends" are for futures/streams, which avoids some ambiguity I've noticed when "handles" was used for both.
  • The internal stream interface (that abstracts over wasm-vs-host-implemented streams) switched to using callbacks in a way that more-clearly/precisely describes how cancellation and multiple partial reads/writes from the same buffer work in component-to-component cases and eliminates some weird corner cases in the previous code that required non-obvious code.

The semantic changes in the second commit are:

  • Tasks no longer have an implicit waitable set; task.wait/task.poll are removed.
  • Canon built-ins waitable-set.{new,wait,poll,drop} are added. See Explainer.md for summaries/signatures.
  • Waitables have 0-or-1 associated waitable sets; waitables start with no associated waitable set and a new waitable.join built-in is added to set/change/clear it.
  • The callback ABI is changed so that an explicit waitable-set-index can be supplied (via return value). This ended up requiring a few supporting changes:
    • To avoid threading both the "context" and waitable-set-index i32s through each call (using linear memory due to lack of multi-return...), the "context" i32 is removed and replaced by adding "context-local storage" in the form of context.get and context.set built-ins. This is already a pending (breaking) change that would need to be made when we integrate thread.spawn, so doing it now just front-loads the breakage. See Async.md#context-local-storage for a better description.
    • To maintain parity between the callback and non-callback ABIs, the packed return value for the callback ABI now has an opcode saying what to do: exit, yield, wait or poll. For wait/poll, the opcode is bit-packed with the waitable-set-index to wait/poll on.
    • Table indices have the top two bits clear (the max table length is 230-1). While the 4 above opcodes technically fit in 2 bits, this seems very likely to be insufficient in the future. Since 64-bit tables may one day be added to handle very-large cases and since 256M table-entries is larger than many implementations would allow anyways, this commit lowers the max table length to 228-1, thereby giving us 4 total bits.
    • canon lower async's bit-packing scheme is changed to be symmetric with the callback bitpacking scheme (reserving 4 bits for the CallState and putting the CallState in the low bits).
  • Since this (should be) the last breaking change to async before 0.3.0, and since 0.3.x will need a CANCELLED value of CallState, the EventCode enum is renumbered to reserve a spot. Also YIELDED is renamed to NONE and given value 0 to make it a natural sentinel value for the callback ABI after a yield or poll-with-no-event.
  • Since the task.* namespace has been mostly evacuated and since task.backpressure and task.yield are not actually task-related operations (backpressure is component-instance field and yielding (will) yield the current thread, not task), they are renamed to backpressure.set (in anticipation of a possible backpressure.get if there's ever a use case for it) and yield.

Add Waitable and WaitableSet as internal classes used to implement existing semantics

Change ReadableStream abstract interface to avoid coroutines simplify logic

Simplify logic and improve CanonicalABI.md description
@lukewagner lukewagner force-pushed the ws branch 2 times, most recently from 130f78d to d5a3719 Compare January 17, 2025 20:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant