runtime, protocol: abortable() — per-call AbortSignal for host imports (A24) - #243
Merged
Conversation
…s (A24, #241) A23's discard is a statement about delivery: the guest stops waiting, but the host operation keeps running — a discarded socket connect keeps connecting, a discarded timer keeps its callback armed. Amendment A24 (contracts/embedder-api.md) closes the loop with the platform's own cancellation vocabulary. `abortable(fn)` — third mark in the suspending()/deferCancel() family, same two spellings, same refusals, brand `polyengine.abortable/1` — opts an import into receiving a fresh AbortSignal appended after its WIT-declared parameters on every call: dial: abortable((addr, signal) => fetch(url, { signal })) The runtime aborts that signal when, and only when, the call's subtask is discarded by a guest cancellation. The mark controls the signature unconditionally (a marked function always receives a signal, so its arity is stable); the abort is discard-only, so the signal never fires on sync-typed imports, deferCancel imports, eager resolves, or natural completion. The abort is deferred one microtask past the cancel built-in (`Promise.resolve().then`, the bare-shell-safe spelling): host listeners must not run inside a live guest activation, and any settlement the abort provokes (an AbortError rejection) lands on A23's resolved-subtask guards and is discarded like any other late settlement — the ordering "guest observes CANCELLED_BEFORE_RETURNED first, host observes the abort a tick later" is pinned by test. The conventions facade needed a real fix to carry this: #wrapImportFn rebuilt the host argument list from WIT arity, silently dropping anything the executor appended — the signal would have been undefined through instantiate(), the only public entry point. Trailing runtime-minted extras are now forwarded verbatim (no toHost conversion; the loop is empty for every unmarked import), and the embedder-layer abort test is the regression pin that isolates exactly that hunk. Tests: protocol brand suite (all three marks on one function, each predicate sees only its own); host_import_cancel_test.ts pins signal arity/position, the microtask deferral (aborted === false synchronously after the cancel returns, true one tick later), AbortError-rejection inertness, the deferCancel composition (five ticks, never aborted), and natural completion; the cancel-import fixture gains sleep-abort / cancel-abort / run-abortable, timed end-to-end at both the raw exec layer and the embedder conventions layer with a host that clears its timer on abort. Negative controls: abort block removed, deferral made synchronous, and facade forwarding removed each fail exactly the tests that own them. Protocol rides the pending 0.2.3 (unpublished, so the byte-identity guard is vacuous — no further bump). Conventions goldens byte-identical; no breaking/* label: additive protocol surface, and the runtime change only gives marked imports an argument they asked for.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #241. Implements contract amendment A24 — the advisory abort channel left open by #242.
The gap
A23 (#242) made a guest's cancellation of an in-flight host import return
promptly by discarding the result — but discard is about delivery, not
execution. The host operation keeps running: a discarded socket connect
keeps connecting, a discarded timer keeps its callback armed. For the
motivating consumer (polymorph-iroh's relay dial timeout) that's one
leaked connect per timeout, in a redial loop.
What A24 adds (contracts/embedder-api.md, in this diff)
abortable(fn)— the third per-declaration mark alongsidesuspending()and
deferCancel()(same direct-call + stage-3 decorator spellings, sameloud refusals, brand
polyengine.abortable/1): every call to a markedimport receives a fresh
AbortSignalappended after its WIT-declaredparameters, and the runtime aborts it when — and only when — that call's
subtask is discarded by a guest cancellation.
Design points:
cancel()hook (theshape floated on A guest cannot cancel an in-flight host import: the subtask's on_cancel is a no-op #241) cannot say WHICH in-flight call died when several
overlap; a per-call signal is right by construction, and it is the
vocabulary the platform already accepts everywhere (fetch, sockets,
timers).
always receives a signal — stable arity — but the signal only ever fires
on a guest-initiated discard. Inert where discard cannot happen:
sync-typed imports (no subtask handle),
deferCancelimports(cancellation never discards), eager resolves, natural completion.
Instance teardown deliberately does not abort (future amendment
material).
canon_subtask_cancel, i.e. inside a live guest activation; host abortlisteners must not execute there (the issue-Guest callback invoked with null context slot (wit-bindgen async_support.rs:578) — polymorph-tls webcrypto-composed target #24 attribution class, plus
re-entrancy).
Promise.resolve().then, notqueueMicrotask— thebare-engine-shell precedent from the jspi sentinels. Ordering pinned by
test: the guest observes
CANCELLED_BEFORE_RETURNEDfirst, the hostobserves the abort a tick later, and an abort-provoked
AbortErrorrejection lands on A23's resolved-subtask guards — discarded like any
other late settlement.
The fix the review process earned
The conventions facade (
#wrapImportFn) rebuilt host argument lists fromWIT arity, silently dropping anything past it — A24 would have been dead
through
instantiate(), the only public entry point, with the signalparameter forever
undefined. The implementing agent caught it; trailingruntime-minted extras are now forwarded verbatim (the loop is empty for
every unmarked import), and the embedder-layer abort test isolates exactly
that hunk: with the forwarding removed it fails loudly (the import body
traps on
signal.addEventListener) while the raw-exec-layer test stillpasses.
Tests
Protocol brand suite (independence: all three marks on one function);
host_import_cancel_test.ts+6 (signal arity and position, unmarked-importarity control, the microtask deferral pinned from both sides —
aborted === falsesynchronously after the cancel returns,trueonetick later — AbortError inertness,
deferCancelcomposition never aborts,natural completion never aborts); fixture
sleep-abort/cancel-abort/run-abortableprobes timed end-to-end at both layers, with the hostclearing its timer on abort. Three negative controls (abort removed;
deferral made synchronous; facade forwarding removed) each fail exactly
the tests that own them, verified with md5-checked restores.
Versioning and labels
Protocol changes ride the pending unpublished 0.2.3 (byte-identity guard
vacuous — no further bump). Goldens byte-identical. No
breaking/*label:additive protocol surface; the runtime hands marked imports an argument
they asked for and changes nothing for unmarked ones (pinned by the arity
control test).
Gates
just gates— exit 0: runtime 676/0, protocol 73/0, conformance 1257/0(no expectation changes), sched-seeds both seeds, sm/node/bun pinned
shells, chromium + firefox all realms, smoke-tls, smoke-c0, npm/publish
checks, version-guard (reports protocol 0.2.3 as a pending bump).