A7 + #66: component faults are loud on host stream/future ops - #76
Merged
Conversation
A guest trap poisons its instance (the enter/leave bracket stays broken, per definitions.py Store.tick line 597) — so live stream/future ends in the poisoned handle table can never rendezvous again. Before this, a host operation parked on such an end hung forever; one started afterwards could "succeed" against the corpse (a copy nothing will ever read); and a second same-direction host op could rendezvous against the FIRST op's own parked buffer (observed: write resolving 1 with no peer in existence — the #66 repro). The retirement walk (task/streams.ts retireInstanceAsyncEnds): at poison time, every live CopyEnd in the dead table records a failure against its shared object (cause = the trap) and is dropped via dropSharedForTeardown, whose parked-side discipline notifies host ends and healthy guest peers (DROPPED) but silently retires a side parked by the doomed guest itself — notifying it would queue a phantom event into the corpse's waitables and a later tick would assert (review B2). Hooked at every bracket-break site, through an injection seam (scheduler.ts setOnInstancePoisoned — streams.ts cannot be imported from the scheduler without making `CopyEnd extends Waitable` evaluation-order-sensitive): exec/boundary.ts poison() (sync lift, entered set), Store.tick and Thread.resumeWith (trap during a resumed thread), and both FACT cross-component catches (callee side — review B1: a composed callee's trap otherwise unwound to hooks that walk only the caller's chain). Layered delivery, mirroring the StreamProducerError design in the inverse direction: the exec layer still settles DROPPED-shaped (definitions.py parity); the conventions layer rejects with PeerTrappedError (cause chains to the trap; write ops carry `progress`) — pre-op via throwIfFailed, post-await via throwIfPeerTrapped, with the precision that an operation which genuinely COMPLETED before the trap keeps its result (a full write, a read that copied data); Future awaits brand instead of reporting a clean-drop DroppedError. A trapping host IMPORT drops the lifted stream/future args it abandoned (releaseAsyncArgs -> dropForTeardown), so their peers settle with the truthful short count; the err-VALUE path deliberately retains handles. One in-flight operation per host end per direction: a second write while one is parked (or second read, or second future op) throws a TypeError synchronously — the host-side spelling of the CopyEnd busy trap. Read-while-write-parked stays legal (different ends). Contract: embedder-api.md amendment A7, plus a numbering repair — main carried two different "amendment A5"s (#63's stream round-trips and #71's wasi-shims parking kernel, which never joined the Status header); the parking kernel is renumbered to A6 with a header entry, this change takes A7. Fixture: stream-pass grows consume-then-trap, open-then-trap and future-then-trap (gate-sequenced so the host can park before the guest dies) + the async-spawn feature. Tests: trap_retire_test.ts pins the parked-write rejection, data-then-reject reads, post-trap pre-op rejection, future branding, import abandonment (pure shape: short count; mixed shape: branding wins), clean-EOS non-branding, and both double-park guards. Reviewed (two rounds): B1/B2 fixed as above; advisory A6-arm asymmetry documented at dropForTeardown; future coverage added (A1); test-4 comment states the pure-shape rationale (A3). Consumer lockfiles (ports/webrtc, ports/websocket): mechanical catch-up adding the @deltic/translator workspace link from #61. Gates (on the rebased tree, 17 commits absorbed incl. the parking kernel and the iroh-exam retirement): build, test-rust, test-runtime (370), test-wasi-shims (50), test-ct-runner, test-bundle, examples, test-translate, conformance (1254/0, linking directory exercises the FACT hooks, no stale xfails), sched-seeds (1, 4242), test-ports, test-webrtc, shells (sm-pinned; jsc-pinned arch-skipped), browsers (chromium, firefox), websocket-conformance (55/0), smoke-tls, smoke-c0 legs 2+4 (legs 1/3: missing experiment-mosh checkout, pre-existing). Closes #66.
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 #66 (follow-up 1 from the #54/#63 investigation), under contract amendment A7.
The defect class
A guest trap poisons its component instance — the enter/leave bracket stays broken (definitions.py
Store.tickline 597),mayEnteris false forever — so live stream/future ends in the poisoned handle table can never rendezvous again. Empirically (repro in the issue): a hostwriteAllparked on such an end hung forever; the export call rejected loudly but nothing walked back to the parked peer. Worse, the repro exposed that a second same-direction host op could "rendezvous" against the first op's own parked buffer and resolve as if a peer took the data (write resolving1with no peer in existence) — silent corruption, not just a hang.The fix
task/streams.ts retireInstanceAsyncEnds): at poison time, every liveCopyEndin the dead table records a failure against its shared object (cause = the trap) and is dropped viadropSharedForTeardown— host ends and healthy guest peers get the spec-shapedDROPPEDnotification; a side parked by the doomed guest itself is retired silently (notifying it queues a phantom event into the corpse's waitables; a latertickthen asserts — review B2).setOnInstancePoisoned; a scheduler→streams import would makeCopyEnd extends Waitableevaluation-order-sensitive):boundary.ts poison()(sync lift, entered set),Store.tick,Thread.resumeWith, and both FACT cross-component catches (callee side — review B1: a composed callee's trap otherwise unwound to hooks that only walk the caller's chain).StreamProducerErrordesign, inverse direction): exec settles DROPPED-shaped (definitions.py parity); the conventions layer rejects withPeerTrappedError(causechains to the trap; writes carryprogress) — with the precision that an op which genuinely completed before the trap keeps its result, and the fault surfaces on the export call and the handle's next op.Futureawaits brand instead of reporting a clean-dropDroppedError.releaseAsyncArgs→dropForTeardown), so their peers settle with the truthful short count. The err-value path deliberately retains handles (a fallible import returning err is a normal outcome).TypeErrorsynchronously — the host-side spelling of theCopyEndbusy trap. Read-while-write-parked stays legal.Contract numbering repair
main carried two different "amendment A5"s (#63's stream round-trips in the Status header, and #71's wasi-shims parking kernel inline-only). The parking kernel is renumbered to A6 with a proper Status-header entry; this change takes A7.
Fixture + tests
stream-passgrowsconsume-then-trap,open-then-trap, andfuture-then-trap(gate-sequenced so the host parks before the guest dies).trap_retire_test.tspins: parked-write rejection with progress, data-then-reject reads, post-trap pre-op rejection, future branding, import abandonment (pure shape → short count; mixed shape → branding wins), clean-EOS non-branding, both double-park guards.Review + gates
Two reviewer rounds; blockers B1 (FACT sites) and B2 (phantom-event via plain
drop()inreleaseAsyncArgs) fixed, advisories addressed (future coverage, comment precision, documented activity-arm asymmetry). Gates on the rebased tree (17 commits absorbed, incl. the parking kernel): build, test-rust, test-runtime (370), test-wasi-shims (50), test-ct-runner, test-bundle, examples, test-translate, conformance (1254/0 — the linking directory exercises the FACT hooks; no stale xfails), sched-seeds, test-ports, test-webrtc, shells, browsers, websocket-conformance (55/0), smoke-tls, smoke-c0 legs 2+4 (legs 1/3 lack the experiment-mosh checkout here, pre-existing).