Skip to content

Async host import issued from an export's initial activation is lost if its promise settles in the next microtask checkpoint #280

Description

@lannbot

Draft: polyengine issue

Title: Async host import called from an export's initial activation is never delivered if its promise settles in the following microtask checkpoint

Summary

An async-lowered host import called during an async export's initial activation (before any earlier park), whose returned promise settles within the microtask checkpoint right after that activation returns WAIT (having already task.returned), never gets its subtask event delivered. The guest task parks forever. If the same promise settles one macrotask later, everything works.

Observed on rev 22b5d3d from the polyengine-dioxus host (Deno, linkedom, @deltic/runtime/embedder instantiate).

Repro shape

Guest (wit-bindgen 0.60, async-spawn + inter-task-wakeup, Rust): an async func export handle-event whose body

  1. synchronously runs app code that calls a sync host import (a resource constructor) and then spawn_locals a task awaiting an async host import method on that resource (join: async func() -> result<string, error>);
  2. returns (task.return) with nothing else pending.

wit-bindgen's Tasks::poll_next then drains the spawned task, which issues the async import (BLOCKED subtask), and the initial activation returns CALLBACK_CODE_WAIT with the waitable set.

Host: join() is async join() { const r = await this.#outcome; ... } where #outcome is already settled (the script threw synchronously), so the returned promise settles after a couple of microtask hops.

Result matrix, varying how the host promise settles relative to the activation return:

host-side settle timing guest resumed?
already-settled inner promise (2-3 microtask hops) no
after await null in the script (a few more hops) no
after setTimeout(r, 0) yes
same import issued from a later callback (after a stream-write park) yes

Trace

POLYENGINE_DRIVE_TRACE=1 for the failing click:

[drive #21] drive branch=top ready=0 waiting=1{Thread[not-ready]} awaiting=1 hostCalls=1 ...
[drive #22] drive branch=->async(awaiting/pending) ...
[drive #23] driveAsync branch=top ready=0 waiting=1{Thread[not-ready]} awaiting=1 hostCalls=1 ...
[drive #24] driveAsync branch=top ready=2 waiting=2{Thread[READY],Thread[READY]} awaiting=0 hostCalls=1 ... done=true
[drive #25] driveAsync branch=EXIT-done ready=0 waiting=1{Thread[not-ready]} awaiting=1 hostCalls=1 ... done=true

After #25 no driver runs again for this store until unrelated activity, and every later drive shows awaiting=1 ... done=true — an activation promise that never gets serviced. Guest-side stderr confirms the spawned task issued the import and was never resumed (the waker registered on the joined future never fires; no EVENT_SUBTASK reaches the callback).

The hostCalls=1 that persists across the whole run is the mutation-stream retention arm (HostActivity), so at #25 hasRealHostCall is presumably false and the settlement pump (boundary.ts, "The settlement pump: liveness between export calls") does not arm. The export's own driveAsync exits because done() is true (the export has task.returned) while a callback activation is still in awaiting.

Hypothesis: the driver's exit criterion for export calls considers the call resolved and leaves a just-started (or about-to-settle) callback activation for the pump, but the pump only watches real host calls, and the host call in question has already settled and left pendingHostCalls. So neither the driver nor the pump owns the resumption.

Workaround in use

polyengine-dioxus host (host/src/eval.ts) defers recv/join settlement by one macrotask (await new Promise(r => setTimeout(r, 0))) before returning. Costs a macrotask per settlement; to be removed when this is fixed.

Minimal standalone repro

Not yet extracted; the above is reproducible with polyengine-dioxus PR #23's host/tests/eval_component_test.ts after removing the await macrotask() lines in host/src/eval.ts (deno task test then hangs on the click assertions until waitFor times out).

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

    bugSomething isn't workingp1Correctness bugs likely to impact consumers; high-priority missing features

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions