Skip to content

Sandbox event loop has no timers: asyncio.sleep/wait_for/timeout raise NotImplementedError #334

Description

@sd2k

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:

  1. 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.
  2. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestrustPull requests that update rust code

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions