The recommended way (https://github.com/WebAssembly/WASI/blob/main/docs/WitInWasi.md#streams) to pass data from one component to an other is by a combination of io:poll and read.
Sadly this means that the interface is asymmetric, as the sender has to create a non-standard "futex like" pollable with the host and pass it to the reader - and there is significant communication overhead (reader component calls poll, writer calls "wake", reader awakes and invokes the read of the writer component) per transmitted date.
So I sat down and designed a consumer-side resource which the reader passes to the subscribe function and the writer calls the "ready(data)" method on whenever there is new data available. This looks neat on paper, but you introduce a cyclic component dependency as the callback interface is imported by the sender from the reader and the data-source interface is imported by the reader from the sender.
This cyclic dependency isn't supported by the current component tooling. But even if it were, the Rust bindgen will additionally expect an imported resource type when passing arguments to an imported function.
I know this is trying to introduce optimizations from the preview3 async proposal into preview2, but can you think of a way to realize a similar symmetric inter-component callback mechanism with preview2?
first off, that WitInWasi doc is sufficiently out of date that i think we should just remove it and maybe someone can take on the project of rewriting it
its unfortunate that streams arent very nice to use between components in preview 2, but its just the restrictions of the component model type system
elsewhere i discussed "onion composition" between components that need to export io, or interfaces depending on io... let me find the link
Oh, I feared that the "middleware" thread discussed this problem, I was hoping for a nice solution with bidirectional resource method calls and no extensive glue code between the components.
unfortunately, for anything that involves transferring control flow between components (i.e. anything that needs to measure readiness using pollables) theres just not a good way to do it in preview 2
By the way, did I miss this component-to-component-activation pollable in the current WASI propsals, or did simply no-one run into this situation before.
when i say pollable i mean wasi/io.poll.{pollable}
we've been aware of this situation for a while (i remember we had a long discussion about it in may of this year) but we're just sorta stuck getting something that doesnt solve it shipped before we can go into solving it
E.g. resource { constructor(); get-pollable: func() -> pollable; wake: func(); }
to create an io compatible pollable to pass to from the writer the reader
so basically, in preview 2, component composition needs to either be entirely synchronous stuff (e.g. the classic example we use that is a markdown renderer with render: func(string) -> string
or it needs to be onion
right, so you cant export a pollable
from that function without exporting the wasi:io package entirely
a pollable
needs to come from the exact same place that implements wasi:io/poll.poll
and thats basically the "magic" function that takes control flow away until progress is made
so that function must be the "parent" of your component, basically.
in the sense that it provides all the exports of io
and every other package that depends on io
to your component.
(Thinking about an onion way to express communication middleware for publisher/subscriber, including wrapping io)
Thank you for the clarification, I can't wait to see future and stream support in preview3 ...
Creating a pollable/waker pair could be added to io/poll with small additional effort, couldn't it?
How would such a thing be used? If a component is blocked waiting for the pollable, there's no way the waker could be triggered since there's currently no notion of threads or reentrancy in the component model.
Good point, I now realize that I was modeling the components running in separate (cooperative) stacks, which strangely is what my applications require. This can only work if poll switches between contexts, which I took for granted.
Yes, and that's essentially what the service chaining approach I described earlier requires:
Second, you can always do service chaining the old-fashioned way by having the middleware component do an outbound HTTP request to the service component (which the host might optimize if it notices the service can be invoked without using the network at all). I.e. you don't need component composition to do service chaining, and you probably won't want to use it for that until Preview 3.
When you have two components running in separate fibers, it's easy enough to connect their concurrent I/O operations using e.g. a buffered, in-memory channel.
Ok, I see, these are the preview3 fibers. I guess I aim far beyond preview2.
Yeah, I think a lot of us are going to rig up a poor man's version of component model async before the real thing arrives :)
Update: I undid the callback optimizations (very similar to slide 11 of the CM async proposal) which require criss-cross linking and went back to exchanging a pollable. A prototype app worked well.
Thank you for the explanations and support!
PS: I genuinely need multiple stacks because the communicating C++|Rust SOA Applications each bring their own main loop.
PPS: I guess I will be an early adopter of preview3 prototypes.
You may be interested in the prototype I posted in the middleware topic which uses the runtime to do a bit of async cooperation between two components.
Last updated: Dec 23 2024 at 12:05 UTC