-
Notifications
You must be signed in to change notification settings - Fork 99
Make 'async' part of the function type, not a hint #578
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
base: main
Are you sure you want to change the base?
Conversation
| imports in the same manner as [JSPI]. Thus, overall, `async` in WIT and the | ||
| Component Model does not behave like a "color" in the sense described by the | ||
| popular [What Color Is Your Function?] essay. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In what sense does it not behave like coloring? Irrespective of how they're lowered/lifted:
Async imports may only be called from within async exports, right?
To me, that sounds exactly like the virality described in "What Color Is Your Function?".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The way I think about this is that you're correct that functions have colors but it's not as extreme as the essay. You're correct that a sync export cannot call an async import in a blocking fashion, but the reason it's not as extreme as the essay is that it's expected to be conventional that WIT worlds have async exports. In such a situation it doesn't matter if your guest language is doing sync or async things -- it'll "all work out".
So colored in one sense, not colored in a different sense.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried to capture the reasoning for how this isn't coloring here in the PR, but the summary is:
- Just because code is called through an
asyncexport doesn't mean the code has the be rewritten from a synchronous style to asynchronous style (unlike with source-languageasync/await), because you can still use the sync ABI for all imported/exported functions. - Practically speaking, most code will be called through an
asyncexport so it can block all it wants. Mostly only components virtualizing worlds containing sync imports will need to implement sync functions and in this advanced virtualization scenario, you usually need higher-order ("donut") composition (where the parent component both implements the imports of the child and calls the exports of the child). Once we allow recursive reentrance (it's currently a TODO) a component A virtualizing the world of a component B could then have a callstack A--calls-->B--calls-->A, and then A could implement B's sync imports by suspending in the inner A call to the outer A call, just like JSPI allows in browsers today where "A" is "native JS code". Thus sync will actually be able to call async, but only if you do this advanced "donut wrapping" composition.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, if I got it right:
At the component interface boundaries there is coloring. But because we can still mix-and-match ABIs, that coloring doesn't (necessarily) make its way into user code. Assuming the entrypoint export is async, of course.
Are non-async exports allowed to start/spawn async import calls, as long as they don't block on the newly created task?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are non-async exports allowed to start/spawn async import calls, as long as they don't block on the newly created task?
Yep! And once they return their value (via task.return) they can even waitable-set.wait on them to finish.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alright, got it. Makes sense to me then!
alexcrichton
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for writing this up!
dicej
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM. Would you be able to add one or more WAST tests to verify the new trapping behavior (and thread.yield-as-no-op, if feasible), either here or in a follow up PR?
|
@dicej Yep! Is there already a dev release with these traps implemented that I can use to write the tests? |
|
Some time ago you promised not to go down the road of function coloring, yet here we are. Async is the goto keyword of our generation. The concept of asynchronicity belongs on the caller side, not on the callee side. There are alternatives; investing the time and handling it properly now will prevent decades of headaches and pain. |
If you can build this PR branch of Wasmtime, you should be set, e.g. |
OK sorry for commenting before reading the full PR. No requirements would be fine but what does "little requirements" mean and if the caller may or may not call the function in a concurrent fashion (as it should be !) then why add async keyword anyways? |
If we're just talking about source code compiled to run in a component with
It's really just about capturing in the calling contract (= function type), as understood by both the caller and the callee (unlike lifting/lowering ABI options, which are encapsulated impl details of their respective component) the (runtime checked) fact that the callee will not block. |
|
Ok, added a nice beefy |
This PR moves
asyncfrom being a hint that is textually mangled into function names to an optional effect type that is part of a function type. WIT is not changed; just how it's encoded as a component import/export definition. For example,goes from being encoded (when imported) as:
to
Along with (and motivating) this change is a runtime behavioral change: if core wasm "blocks" (viz.: calls
waitable-set.{wait,poll},thread.suspend, or calls something that might block synchronously) and the current task has not returned and the current task's function type does not containasync, there is a trap. Thus,asyncmoves from being a "hint" that can be violated to an invariant that a client (host or component) can depend on.The motivation for this change is working through the browser/JS embedding (both in a near-term
jco transpilecontext but also considering a longer-term native browser/JS embedding). If the lack ofasyncdoesn't guarantee the lack of blocking, then a browser/JS embedding would never be able to call a non-asynccomponent export in a synchronous context (with important use cases being: anaddEventListenercallback, constructors, getters and setters). This is certainly the case forjco transpileimplemented in terms of JSPI (where "blocking" manifests as the JS glue code calling into wasm that returns aPromisethat cannot beawaited in a synchronous JS context). But even in a native browser implementation, blocking in a synchronous context basically requires spinning a nested event loop which browsers are generally trying to kill off and prevent any new occurrences of. This change would also be beneficial for other, non-browser embeddings which have the same underlying implementation constraints as browsers.This change also reduces the cognitive overhead for bindings generators and power users of the hint. As a hint, since a non-
asyncimport might block, the bindings generator had to ask: "do I, and how do I, expose a power-user option for overriding the hint to handle the blocking case". (This is especially tricky when considering native binding support, as we'd eventually like for all embeddings of components into language runtimes, where there's not the option of "build flags"). Conversely, if I'm a very careful programmer who wants to achieve maximum concurrency: ifasyncis just a hint and callees "might" always block: should I override the hint and when? Do I just determine when to override experimentally or conservatively always use async? Having a checked effect avoids developers wasting their time with these obscure-but-otherwise-necessary questions.Although this change would seem to give functions a "color", for the reasons described in this PR in Concurrency.md#summary,
asyncat the WIT/Component level does not have the same infective quality as source-languageasync. This change is also backwards compatible with existing Preview 2 components (which are all non-asyncand have no way to block) and existing Preview 3 WASI worlds (which already mark all the exports asasyncand can thus always block, as you'd expect).