Every relay dial carries a deadline; a stalling relay cannot pin the endpoint - #95
Merged
Conversation
…endpoint A relay (or middlebox) that accepted TCP and then stalled the handshake pinned all three dial sites forever: bind hung its caller, ensure-relay held the relay-opening claim so the URL stayed permanently "opening", and the home-relay redial slot pinned until process restart (#93). The dials shipped unbounded because dropping an in-flight import future in-task wedged the polyengine host (polymorph-components/polyengine#239); that is fixed as of polyengine 0.5.1 (A23: a guest's cancel of a host import is a prompt discard), so the timeout returns as the plain select it always wanted to be. - DIAL_TIMEOUT (10s) matches upstream's relay connect timeout (iroh-1.0.3 src/socket/transports/relay/actor.rs CONNECT_TIMEOUT) and sits under ensure-relay's 30s claim-wait. dial_relay races RelayConn::connect against the clock and drops the loser; all three sites use it. The deadline surfaces as the WIT `timed-out` variant; other dial failures stay `connect-failed`. - The "do not add a timeout here" rulings on pump/redial_home (and the module doc's never-dropped-mid-flight claim) are repealed; the teardown resolve-before-return discipline stays as hygiene. - polyengine floor 0.5.1 in host-polyengine (caret floor + both locks + the README pin note): under 0.5.0 the first timed-out dial would wedge the consumer's store, so the floor is correctness, not housekeeping. - Exam scenario 8 is the gate: a stub that accepts and never writes; bind and two foreign-relay connects must fail timed-out inside the deadline window (the second connect proves the claim released), and the home-relay redial must keep redialing through the stall and recover once a real relay returns, with no guest trap across the dropped dials. Known residue: on the polyengine host a timed-out dial's host-side connect keeps running until it settles on its own (A23 discard is delivery-only). The fix is marking polymorph:websocket's connect abortable() (polyengine A24), a sibling PR. Falsified against the unfixed endpoint: scenario 8 fails on both attempts with `timed out after 30000 ms: leg A bind`. Gates: just check, just build, just exam-polyengine (9/9, leg C exercised with an owned relay), just matrix (13/13). Fixes #93
lannbot
enabled auto-merge
August 23, 2026 19:09
This was referenced Aug 23, 2026
Merged
Merged
lannbot
pushed a commit
that referenced
this pull request
Aug 23, 2026
Patch release carrying the relay-robustness line landed since 0.5.0: home-relay failure is a redial rather than endpoint death (#91), every relay dial carries a deadline (#95), and quiet relay wires are pinged with unanswered pings retiring the connection (#97). No WIT or JS surface change; the published @polyengine/runtime floor is already ^0.5.1 (A23 prompt-discard of dropped import futures, polyengine#239). Gates: build-components + deno publish --dry-run clean; full CI on the PR.
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.
Every relay dial in the endpoint was unbounded (#93): a relay — or any middlebox — that accepts the TCP/websocket connection and then stalls the relay handshake pinned the dial forever. Per site:
bindhung its caller;ensure_relayheld therelay_openingclaim so every later dial of that URL waited 30s and failed while the relay stayed permanently "opening"; the home-relay redial slot pinned, leaving the endpoint alive but home-dead until process restart.Why it ships now
The dials were unbounded by ruling, not omission: bounding one means racing the connect against a timer and dropping the loser, and dropping an in-flight import future inside a live task wedged the polyengine host permanently (polymorph-components/polyengine#239). That chain is resolved as of polyengine 0.5.1:
So resolution path 1 from #93 (“upstream fix first”) is taken: the timeout is a plain select, the shape written and reverted during the #88 work.
The fix
DIAL_TIMEOUT= 10s, matching upstream's relay connect timeout (iroh-1.0.3src/socket/transports/relay/actor.rs,CONNECT_TIMEOUT) and sitting well underensure_relay's 30s claim-wait, so a stalled dial resolves its waiters before they give up.dial_relayracesRelayConn::connectagainst the clock and drops the loser;bind,ensure_relay, andredial_homeall dial through it. The deadline surfaces as the WITtimed-outvariant; the dial's own failures stayconnect-failed. A timed-out redial counts as a failed attempt and the pump rearms the next redial with backoff.pumpandredial_home, and the module doc's "never dropped mid-flight" claim, now state the A23 contract. The teardown resolve-before-return discipline is retained as hygiene, not a correctness requirement.host-polyengine/deno.jsoncaret floor, both deno.locks, the README pin note): under 0.5.0 the first timed-out dial would wedge the consumer's store, so the floor is correctness, not housekeeping. Protocol resolves to 0.2.3 in lockstep across both locks (the exam's identity gate asserts singularity).#93's open item on the wasmtime host resolves by construction: the guest-side deadline bounds the dial regardless of host connect behavior, and on wasmtime the drop is a real cancellation of the host future. The matrix exercises that path — every successful dial now drops the losing timer future in-task.
Known residue (not this PR): on the polyengine host a timed-out dial's host-side connect keeps running until it settles naturally — A23's discard is delivery-only. The fix is marking
polymorph:websocket'sconnectabortable()(A24, polyengine#243, which names this repo as the motivating consumer); that is a sibling PR to polymorph-websocket, queued next.The regression guard
Exam scenario 8, "stalling relay": a stub that accepts TCP, drains, and never writes.
bindagainst the stub must rejecttimed-outinside[9.5s, 20s].connectwhose only path is a stalling foreign relay must rejecttimed-outin-window, and a second connect through the same relay must do the same — before this fix the first dial never released its claim, so the second waited out the 30s claim instead of dialing.Falsified against the unfixed endpoint: with the endpoint change stashed and the component rebuilt, scenario 8 fails both attempts with
timed out after 30000 ms: leg A bindand the exam reportsEXAM FAIL (1 failing scenario(s)).Gates
just check(fmt, clippy, validate-wit, test): passjust build: passjust exam-polyengine: 9/9 scenarios pass with an owned relay — scenario 8:bind timed out at 10005 ms; leg B claim released; redial re-armed and recovery echo OKjust matrix: all 13 pairings passFixes #93