ignatz opened issue #11584:
Hey folks,
I've been bumbling around the code-base trying to keep "pumping"/polling an instance with pending timers.
In essence, I'm using wasi_http to send handle an HTTP request with a guest. So far so good. If the handler installs a timer I would like to keep polling beyond
let response = proxy .wasi_http_incoming_handler() .call_handle(&mut store, req, out) .await; ``` I assume timers are captures in the store's `resource_table`. However IIUC, the `resource_table` is deliberately opaque, so I don't have a particular resource to poll. Is there any other particular way for me to get timers and/or keep polling? ~~~
ignatz edited issue #11584:
Hey folks,
I've been bumbling around the code-base trying to keep "pumping"/polling an "instance" for pending timers.
In essence, I'm using wasi_http to send handle an HTTP request with a guest. So far so good. If the handler installs a timer I would like to keep polling beyond
let response = proxy .wasi_http_incoming_handler() .call_handle(&mut store, req, out) .await; ``` I assume timers are captures in the store's `resource_table`. However IIUC, the `resource_table` is deliberately opaque, so I don't have a particular resource to poll. Is there any other particular way for me to get timers and/or keep polling to make sure the timers get a change to trigger in time? ~~~
ignatz edited issue #11584:
Hey folks,
I've been bumbling around the code-base trying to keep "pumping"/polling an "instance" for pending timers.
In essence, I'm using wasi_http to send handle an HTTP request with a guest. So far so good. If the handler installs a timer I would like to keep polling beyond
let response = proxy .wasi_http_incoming_handler() .call_handle(&mut store, req, out) .await; ``` I assume timers are captures in the store's `resource_table`. However IIUC, the `resource_table` is deliberately opaque, so I don't have a particular resource to poll. Is there any other particular way for me to get timers and/or keep polling to make sure the timers get a chance to trigger in time? ~~~
alexcrichton commented on issue #11584:
Could you provide example code or describe in more detail the problem that you're running into? For example is a future getting "stuck"? Is something not making progress? The entirety of the guest's computation, regardless of whether it's internally using timers or anything else, is captured in the
Futureimplementation returned bycall_handle. In that sense your questions are difficult to answer because the answer is "you do nothing and it should work". That's what leads me to ask for more detail or more description of the problem.
ignatz commented on issue #11584:
Hey @alexcrichton , thanks for the super reply :folded_hands:
Could you provide example code or describe in more detail the problem that you're running into? For example is a future getting "stuck"? Is something not making progress? The entirety of the guest's computation, regardless of whether it's internally using timers or anything else, is captured in the Future implementation returned by call_handle. In that sense your questions are difficult to answer because the answer is "you do nothing and it should work".
Absolutely. Nothing is stuck. It does just work... maybe too well :) - let me expand...
A strawman guest may look something like:
export const incomingHandler = { handle: function ( req: IncomingRequest, respOutparam: ResponseOutparam, ) { setInterval(() => { console.log("Timer expired") }, 1000); ResponseOutparam.set(responseOutparam, { tag: "ok", val: /* ... */ }); }i.e. from the host's side
call_handle's Future will be fulfilled right away. It works, no problem :partying_face: . However, I'm interested in "side-effects". The example may be silly, but the call could for example trigger a periodic refresh, fetch something and write it to a DB.
This notion of starting something which may have side-affects and then drive them to completion may feel like an antithesis to the pure functional nature ofcall_handler(). Besides the intrinsic issues of resource leakages, infinite loops, ... is this something that would be possible?(In this picture, I'm owning the host/emebedder running someone else's code using my abstractions so a solution that involves calling back into the host before returning from the http handler would work for me)
bjorn3 commented on issue #11584:
As far as I understand, if the component spawned a sub task for running the
setInterval, then thewasi_http_incoming_handler()task would block until this sub task has finished before returning to the host. If on the other hand the component were to manually poll the timer future without spawning a sub task for thesetInterval, then you did have to call whichever code is responsible for this polling (javascript event loop?) from the host. Wasmtime can't do this for you as it depends on the exact way the component is implemented.
alexcrichton commented on issue #11584:
I fear there's no short answer to your question here @ignatz so I'll try to dissect the various pieces here to explain what's going on. At first I'm assuming everything being done here is with WASIp2. With WASIp2 there's no concept of async in WIT/components itself which is important because all Wasmtime as a host knows is "did the function return or not". So the first thing we can possibly do is to rephrase your question in terms of what the guest is doing.
If a JS guest wants to support
setIntervalthen the JS guest is the one faced with the question of "the entrypoint has returned and there's pending work insetInterval, what should happen?" The exact answer here depends on the runtime, but let's assume for now that the component-level function returns. This means that Wasmtime now has the result ofcall_handle, for example. Wasmtime has no means of knowing, however, that the guest hassetIntervaltimers. Thus in this world there's no knowledge of what to do and the timers will just never fire.Solving this could be done with one of a few possible approaches. First is that the guest could avoid returning until all timers are done. This means that Wasmtime wouldn't ever return from
call_handleuntil all timers are cleared. For WASIp2 HTTP this may not be so bad sincesetis where the response is sent. Second is that the guest and host could have a higher-level protocol between themselves. This would be outside the WASI standardization space but the guest could tell the host "hey call me back in a few seconds" and then has another entrypoint. This would likely be difficult to thread everywhere though due to the non-standard nature. Third though brings me to...All of this looks a little different in WASIp3. In WASIp3 the component model and WIT have native knowledge of async operations. There is a distinct concept of returning from a function and then exiting a task. This would mean that your example could be modeled as returning from the function and then continuing to execute work within the task so long as there are timers. Wasmtime's implementation of WASIp3, however, means that
call_handlewon't actually return until the component level task exits. This means that without further changes to the host API Wasmtime wouldn't be able to model this super well. I'll open an issue about this after I finish writing this comment.That's a lot of words to say, however, "this should work by default". What you're describing seems quite reasonable to me to expect to work by default without needing much interactions. How exactly that's modeled on the host is going to be a bit tricky. With WASIp2 it in theory should work already, and we'll need some changes to make it work with WASIp3.
@bjorn3
if the component spawned a sub task for running the setInterval
To clarify, WASIp2 has no concept of tasks for anything here. Given that tasks are either purely a host abstraction (e.g. a Tokio task) or a guest abstraction (e.g. a turn of the JS event loop). With that in mind I find your response somewhat difficult to interpret. In the context of WASIp3, however, there are definitely component-model level tasks, so is that what you were referring to?
bjorn3 commented on issue #11584:
Yeah, I was referring to wasip3.
ignatz commented on issue #11584:
I'll try to dissect the various pieces here to explain what's going on.
:folded_hands:
At first I'm assuming everything being done here is with WASIp2.
Affirmative
If a JS guest wants to support
setIntervalthen the JS guest is the one faced with the question of "the entrypoint has returned and there's pending work insetInterval, what should happen?" The exact answer here depends on the runtime, but let's assume for now that the component-level function returns. This means that Wasmtime now has the result ofcall_handle, for example. Wasmtime has no means of knowing, however, that the guest hassetIntervaltimers. Thus in this world there's no knowledge of what to do and the timers will just never fire.:+1: that's where I'm at.
Solving this could be done with one of a few possible approaches. First is that the guest could avoid returning until all timers are done. This means that Wasmtime wouldn't ever return from
call_handleuntil all timers are cleared. For WASIp2 HTTP this may not be so bad sincesetis where the response is sent.This is the one I'm aware of, i.e. just keep polling the original future and everything will fall into place. That said, I would like to decouple.
Example: you're implementing some sort of message queue, you're submitting work, want it to be acknowledged and then pick up the result later on. Maybe even by a different caller or with connection interrupts, so keeping a stream open isn't ideal.Second is that the guest and host could have a higher-level protocol between themselves. This would be outside the WASI standardization space but the guest could tell the host "hey call me back in a few seconds" and then has another entrypoint. This would likely be difficult to thread everywhere though due to the non-standard nature.
let's say the guest's
mySetIntervalcalls the host to tell it timers have been installed. The host then calls the guest, e.g. aftercall_handlecompletes... but how would I ensure that that follow-up call only resolves after the last timer has expired or cancelled and the continuations get to run? That's sort of the functionality I was digging for. V8 for example has methods to check if there are ready events on the event-loop and "pump" it. Arguably, WASM doesn't have this kind of event-loop, which is probably at the heart of the problem, i.e. there isn't a clear entry-point for the host to call for the ambient computation to continue.All of this looks a little different in WASIp3. In WASIp3 the component model and WIT have native knowledge of async operations. There is a distinct concept of returning from a function and then exiting a task. This would mean that your example could be modeled as returning from the function and then continuing to execute work within the task so long as there are timers. Wasmtime's implementation of WASIp3, however, means that
call_handlewon't actually return until the component level task exits. This means that without further changes to the host API Wasmtime wouldn't be able to model this super well. I'll open an issue about this after I finish writing this comment.:folded_hands:
Sounds like whether with WASIp2 or WASIp3 the approach would be to model this behavior as "don't return so the host has something that it can continue to poll and therefore advance the guest". In this case, the end of an HTTP stream would have to be modeled not implicitly by returning but as an explicit stream-end marker. Does that match your thinking?
That's a lot of words to say, however, "this should work by default". What you're describing seems quite reasonable to me to expect to work by default without needing much interactions.
That would be wonderful (but suggests that my understanding in the previous paragraph is likely flawed - would love to be wrong :) ).
How exactly that's modeled on the host is going to be a bit tricky. With WASIp2 it in theory should work already, and we'll need some changes to make it work with WASIp3.
How to model it is definitely where I'm struggling. If it's already possible in WASIp2, maybe you could expand a little on how. Would this be the "second" approach, you mentioned, where the host calls back the guest?
Thank you so much for your time and the wealth of information :folded_hands:
alexcrichton commented on issue #11584:
Ok thanks @bjorn3, makes sense! I think the confusion then is that I don't think @ignatz was originally talking about wasip3 but you were, or at least that resolves my own personal confusion.
After talking with @dicej I can correct my understanding of things. @ignatz I've opened https://github.com/bytecodealliance/wasmtime/issues/11600 for the WASIp3 side of things, which further codifies the "true answer" here as "the guest completes but does not return". Put another way:
- WASIp2: today you'll call
setwhen your headers are ready. You'll finish up the body via the WASI resources you made and that'll be doing I/O and such. Then later you'll eventually actually exit. Blocking is done viawasi:io/pollwhich informs the host "here's a list of things the guest is waiting on". Through this there's no guest/host communication or callbacks or such, or rather it's sort of implicit viawasi:io/poll. This is why WASIp2 should "just work" today with appropriately configured guests (e.g. returning is blocked until all pending work is finished)- WASIp3: it'll all work the same way except things are a bit more formal in the component model. The guest will return as it does today, and then with #11600 the embedder will be able to further await the guest exiting its task entirely. Here again there's not really any host/guest protocol other than the component model itself where the guest will inform the host via intrinsics "I'm waiting on this timer" or something like that, and the host's await of the task's end will naturally wait for its conclusion in the guest.
I realize WASIp3 isn't necessarily fully documented, but does the WASIp2 bits make more sense? I realize I'm refining my own understanding of this over time as well so I'm a bit rambly...
ignatz closed issue #11584:
Hey folks,
I've been bumbling around the code-base trying to keep "pumping"/polling an "instance" for pending timers.
In essence, I'm using wasi_http to send handle an HTTP request with a guest. So far so good. If the handler installs a timer I would like to keep polling beyond
let response = proxy .wasi_http_incoming_handler() .call_handle(&mut store, req, out) .await; ``` I assume timers are captures in the store's `resource_table`. However IIUC, the `resource_table` is deliberately opaque, so I don't have a particular resource to poll. Is there any other particular way for me to get timers and/or keep polling to make sure the timers get a chance to trigger in time? ~~~
ignatz commented on issue #11584:
I realize WASIp3 isn't necessarily fully documented, but does the WASIp2 bits make more sense? I realize I'm refining my own understanding of this over time as well so I'm a bit rambly...
Really appreciate the time, glad if it's useful to you too
WASIp2: today you'll call set when your headers are ready. You'll finish up the body via the WASI resources you made and that'll be doing I/O and such. Then later you'll eventually actually exit. Blocking is done via wasi:io/poll which informs the host "here's a list of things the guest is waiting on". Through this there's no guest/host communication or callbacks or such, or rather it's sort of implicit via wasi:io/poll. This is why WASIp2 should "just work" today with appropriately configured guests (e.g. returning is blocked until all pending work is finished)
After reading this a few more times than I like to admit, I know see my fallacy. I can confirm that
.set()completes the HTTP response independent of the incoming-handler returning. So as long as one manages to keep track of pending tasks, we can continue to drive the tasks from the handler's body. I'm carefully optimistic that this solves my problem :folded_hands:I'm not sufficiently up-to-speed on WASIp3 to appreciate the nuances but it sounds like there will be more of a first-class Task concept that the embedder can explicitly drive to completion. If all I/O would be handled this way, this may solve the issue where I don't manage to keep track of all pending tasks a user's code may issue. That would be delightful :folded_hands:
Thanks again!
ignatz edited a comment on issue #11584:
I realize WASIp3 isn't necessarily fully documented, but does the WASIp2 bits make more sense? I realize I'm refining my own understanding of this over time as well so I'm a bit rambly...
Really appreciate the time, glad if it's useful to you too
WASIp2: today you'll call set when your headers are ready. You'll finish up the body via the WASI resources you made and that'll be doing I/O and such. Then later you'll eventually actually exit. Blocking is done via wasi:io/poll which informs the host "here's a list of things the guest is waiting on". Through this there's no guest/host communication or callbacks or such, or rather it's sort of implicit via wasi:io/poll. This is why WASIp2 should "just work" today with appropriately configured guests (e.g. returning is blocked until all pending work is finished)
After reading this a few more times than I like to admit, I now see my fallacy. I can confirm that
.set()completes the HTTP response independent of the incoming-handler returning. So as long as one manages to keep track of pending tasks, we can continue to drive the tasks from the handler's body. I'm carefully optimistic that this solves my problem :folded_hands:I'm not sufficiently up-to-speed on WASIp3 to appreciate the nuances but it sounds like there will be more of a first-class Task concept that the embedder can explicitly drive to completion. If all I/O would be handled this way, this may solve the issue where I don't manage to keep track of all pending tasks a user's code may issue. That would be delightful :folded_hands:
Thanks again!
Last updated: Dec 06 2025 at 06:05 UTC