Problem
_EryxLoop (crates/eryx-wasm-runtime/src/python.rs, ~L1063) is a minimal asyncio.AbstractEventLoop that implements only call_soon, create_task, create_future and friends. time() is an explicit stub:
def time(self): raise NotImplementedError
and there is no call_later/call_at. Consequently every timer-based asyncio API is unusable inside the sandbox. Verified empirically against the current build:
| API |
Result |
asyncio.sleep(0.01) |
NotImplementedError |
asyncio.wait_for(coro, timeout=5) |
NotImplementedError |
async with asyncio.timeout(5) |
NotImplementedError |
asyncio.get_event_loop().time() |
NotImplementedError |
asyncio.gather(...) |
works (no timers involved) |
This is a real gap for user code: asyncio.sleep and wait_for are extremely common in async Python, including retry/backoff loops and any library that bounds its own operations.
Reading the clock already works
Worth noting the clock is not the missing piece. Inside the sandbox all of these already work, via libwasi-emulated-process-clocks.so → preview1 adapter → clock_time_get:
time.monotonic() # ok (0.007311926)
time.time() # ok (1787058857.31)
time.perf_counter() # ok (0.018573705)
time.sleep(0.01) # ok (blocking)
So _EryxLoop.time() is close to a one-liner (return time.monotonic()). The substantive work is scheduling: call_later/call_at need a way to wake the guest when a deadline expires.
Suggested approach
The loop is driven by host resumption through waitable sets (waitable_set_new_ / waitable_join_, and the _await_net_result future plumbing). A timer needs to become just another waitable in that set. Two options:
- Import
wasi:clocks/monotonic-clock into the sandbox world (crates/eryx-runtime/wit/runtime.wit) and use subscribe-duration, which returns a pollable that can join the waitable set. Standard component-model approach; the world currently imports only eryx:net/* plus eryx host functions, no wasi:clocks.
- Add an eryx host timer import (e.g.
import sleep: async func(duration-ms: u64)) returning a waitable that resolves after the duration, reusing the existing net-op async plumbing verbatim.
(1) is more standard; (2) is more consistent with the existing architecture and probably less new surface. Either way, note the guest-import arg-marshalling constraints that bit us before (#240).
Things to get right
- A timer must not extend the execution budget. The host's
execution_timeout must still win; asyncio.sleep must not become a way to sit on a slot indefinitely.
- Timers can only fire when the loop regains control. A deadline cannot preempt a blocking synchronous hostcall — worth documenting the limitation rather than implying full preemption.
- Interaction with fuel metering: a sleeping task shouldn't burn fuel.
- Snapshot/restore: a pending timer crossing a snapshot boundary needs defined behaviour.
Why this came up
Split out of #333. test_async_tls_api_external needed per-step timeouts; the natural fix (asyncio.wait_for around each net op inside the sandbox) is impossible for the reasons above, so that PR bounded the operations with NetConfig(connect_timeout_ms=..., io_timeout_ms=...) from the host side instead. Fixing this issue would let sandboxed code bound its own operations directly, and would let _eryx_async's shim expose the timeout_ms parameter the Rust layer already accepts (_eryx_tcp_connect(host, port, timeout_ms=0)) but which the Python shim never passes.
Possibly related: #244 (deterministic replay / mockable clock) — a timer implementation should be compatible with whatever clock-mocking design lands there.
Problem
_EryxLoop(crates/eryx-wasm-runtime/src/python.rs, ~L1063) is a minimalasyncio.AbstractEventLoopthat implements onlycall_soon,create_task,create_futureand friends.time()is an explicit stub:and there is no
call_later/call_at. Consequently every timer-based asyncio API is unusable inside the sandbox. Verified empirically against the current build:asyncio.sleep(0.01)NotImplementedErrorasyncio.wait_for(coro, timeout=5)NotImplementedErrorasync with asyncio.timeout(5)NotImplementedErrorasyncio.get_event_loop().time()NotImplementedErrorasyncio.gather(...)This is a real gap for user code:
asyncio.sleepandwait_forare extremely common in async Python, including retry/backoff loops and any library that bounds its own operations.Reading the clock already works
Worth noting the clock is not the missing piece. Inside the sandbox all of these already work, via
libwasi-emulated-process-clocks.so→ preview1 adapter →clock_time_get:So
_EryxLoop.time()is close to a one-liner (return time.monotonic()). The substantive work is scheduling:call_later/call_atneed a way to wake the guest when a deadline expires.Suggested approach
The loop is driven by host resumption through waitable sets (
waitable_set_new_/waitable_join_, and the_await_net_resultfuture plumbing). A timer needs to become just another waitable in that set. Two options:wasi:clocks/monotonic-clockinto thesandboxworld (crates/eryx-runtime/wit/runtime.wit) and usesubscribe-duration, which returns apollablethat can join the waitable set. Standard component-model approach; the world currently imports onlyeryx:net/*plus eryx host functions, nowasi:clocks.import sleep: async func(duration-ms: u64)) returning a waitable that resolves after the duration, reusing the existing net-op async plumbing verbatim.(1) is more standard; (2) is more consistent with the existing architecture and probably less new surface. Either way, note the guest-import arg-marshalling constraints that bit us before (#240).
Things to get right
execution_timeoutmust still win;asyncio.sleepmust not become a way to sit on a slot indefinitely.Why this came up
Split out of #333.
test_async_tls_api_externalneeded per-step timeouts; the natural fix (asyncio.wait_foraround each net op inside the sandbox) is impossible for the reasons above, so that PR bounded the operations withNetConfig(connect_timeout_ms=..., io_timeout_ms=...)from the host side instead. Fixing this issue would let sandboxed code bound its own operations directly, and would let_eryx_async's shim expose thetimeout_msparameter the Rust layer already accepts (_eryx_tcp_connect(host, port, timeout_ms=0)) but which the Python shim never passes.Possibly related: #244 (deterministic replay / mockable clock) — a timer implementation should be compatible with whatever clock-mocking design lands there.