Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 68 additions & 29 deletions endpoint/src/endpoint_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@
//! timeout, signaling deadline) is observed, and how stale a missed
//! wake edge can go.
//!
//! An in-flight import is a component-model subtask and is always
//! awaited to completion, never dropped mid-flight (the teardown
//! discipline; the kick future is guest-local, so re-creating it each
//! select turn cancels nothing). All of it runs on the component-model
//! An in-flight import is a component-model subtask; dropping one is a
//! `subtask.cancel`, which the minimum hosts (polyengine 0.5.1 / A23,
//! wasmtime) settle promptly, so a select race may drop its loser (the
//! kick future is guest-local, so re-creating it each select turn
//! cancels nothing). All of it runs on the component-model
//! async ABI's single cooperative thread: the `RefCell` borrows never
//! cross an await, and a fired waker resumes its task through the
//! scheduler, never synchronously.
Expand Down Expand Up @@ -235,6 +236,15 @@
/// restarts the backoff, a younger one consumes the next delay.
const REDIAL_ESTABLISHED: Duration = Duration::from_secs(10);

/// Deadline on every relay dial (`bind`, `ensure-relay`, the home
/// redial): a relay that accepts the socket and then stalls the
/// handshake fails the dial instead of pinning it (issue #93). The
/// value matches upstream's relay connect timeout (iroh-1.0.3
/// src/socket/transports/relay/actor.rs, CONNECT_TIMEOUT) and sits
/// well under `ensure_relay`'s 30s claim-wait, so a stalled dial
/// resolves its waiters before they give up.
const DIAL_TIMEOUT: Duration = Duration::from_secs(10);

struct ChannelEntry {
wire: Rc<ChannelWire>,
/// The relay-authenticated peer the channel was signaled with;
Expand Down Expand Up @@ -783,19 +793,20 @@
/// The endpoint's I/O task: relayed datagrams in and out, noq's timers,
/// the flush after every kick, and the home relay's redial. The two
/// long-lived import futures stay pinned across iterations and are
/// resolved before the task returns (an in-flight import is a
/// component-model subtask; jco traps on cancelling one — the teardown
/// discipline). Channel receives live in a persistent set with the same
/// discipline: closing every channel resolves them before the task
/// returns. The redial slot is the exception: an in-flight relay dial is
/// dropped at teardown rather than awaited.
/// resolved before the task returns; channel receives live in a
/// persistent set with the same discipline: closing every channel
/// resolves them before the task returns. The redial slot is the
/// exception: an in-flight relay dial is dropped at teardown rather
/// than awaited.
///
/// The two kinds of drop are not the same hazard. Dropping an in-flight
/// import to settle a select race INSIDE a live task wedges the host,
/// which sees an activation that never parks, finishes, or traps (see
/// [`redial_home`]). Dropping one as the whole task ends is the
/// host-driven cancellation `ensure_relay` already takes when its call
/// is cancelled, and is sound.
/// Dropping an in-flight import future is a component-model
/// `subtask.cancel`, and is safe both mid-task (settling a select
/// race, as [`dial_relay`]'s deadline does) and at task end. The
/// minimum host is part of that claim: polyengine settles the cancel
/// as a prompt discard only from 0.5.1 (A23; earlier versions wedge
/// the store, polyengine#239). The resolve-before-return discipline
/// above is retained as teardown hygiene, not a correctness
/// requirement.
///
/// A failed home relay retires its connection from the pool and arms a
/// redial; the pump ends only on close, never on a wire failure.
Expand Down Expand Up @@ -1172,17 +1183,36 @@

type RedialFuture = futures::future::LocalBoxFuture<'static, Result<RelayConn, String>>;

/// A failed [`dial_relay`]: the deadline, or the dial's own failure.
enum DialError {
/// The dial did not resolve within [`DIAL_TIMEOUT`].
TimedOut,
/// The dial resolved with a failure.
Failed(String),
}

/// One relay dial bounded by [`DIAL_TIMEOUT`]: `RelayConn::connect`
/// raced against the clock, the loser's in-flight future dropped.
/// The drop is a component-model `subtask.cancel`; the minimum hosts
/// (polyengine 0.5.1 / A23, wasmtime) settle it promptly — a discard
/// or a real cancellation. On a discard the host-side connect may
/// still run to its natural end; only delivery is renounced.
async fn dial_relay(url: &str, identity: &Identity) -> Result<RelayConn, DialError> {
let mut dial = pin!(RelayConn::connect(url, identity).fuse());
let mut timer = pin!(monotonic_clock::wait_for(DIAL_TIMEOUT.as_nanos() as u64).fuse());
select_biased! {
opened = dial => opened.map_err(DialError::Failed),
_ = timer => Err(DialError::TimedOut),
}
}

/// One home-relay redial: wait out the backoff, then connect and
/// authenticate.
///
/// The dial carries no timeout of its own, and must not: a timeout is a
/// second future racing the dial, so every attempt drops whichever of
/// the two loses — the in-task select-race drop [`pump`]'s doc rules
/// out. Dropping the timer that way wedges the polyengine host, which
/// reports it as a resumed activation whose claim is never released. A
/// relay that accepts the socket and then stalls the handshake
/// therefore holds this slot until the socket resolves; nothing else in
/// the endpoint waits on it.
/// The dial is bounded by [`DIAL_TIMEOUT`] (via [`dial_relay`]): a
/// relay that accepts the socket and then stalls the handshake fails
/// the attempt, and the pump's error arm rearms the next redial with
/// backoff (issue #93).
async fn redial_home(
url: String,
identity: Rc<Identity>,
Expand All @@ -1191,7 +1221,10 @@
if !delay.is_zero() {
monotonic_clock::wait_for(delay.as_nanos() as u64).await;
}
RelayConn::connect(&url, &identity).await
dial_relay(&url, &identity).await.map_err(|e| match e {
DialError::TimedOut => "relay dial timed out".to_string(),
DialError::Failed(e) => e,
})
}

type ChannelRecvFuture =
Expand Down Expand Up @@ -1545,15 +1578,18 @@
normalized: normalized.clone(),
armed: true,
};
let opened = RelayConn::connect(url, &self.identity).await;
let opened = dial_relay(url, &self.identity).await;
claim.armed = false;
let mut st = self.shared.borrow_mut();
st.relay_opening.remove(&normalized);
// Concurrent dialers of this relay wait on the outcome.
st.wake_waiters();
return match opened {
Ok(conn) => Ok(st.register_relay(url, Rc::new(conn))),
Err(e) => Err(Error::ConnectFailed(format!("relay {url}: {e}"))),
Err(DialError::TimedOut) => {
Err(Error::TimedOut(format!("relay {url}: dial timed out")))
}
Err(DialError::Failed(e)) => Err(Error::ConnectFailed(format!("relay {url}: {e}"))),
};
}
let started = Instant::now();
Expand Down Expand Up @@ -1645,9 +1681,12 @@
));
}

let relay = RelayConn::connect(&relay_url, &identity)
let relay = dial_relay(&relay_url, &identity)
.await
.map_err(Error::ConnectFailed)?;
.map_err(|e| match e {
DialError::TimedOut => Error::TimedOut("relay dial timed out".into()),
DialError::Failed(e) => Error::ConnectFailed(e),
})?;

let mut reset_key = [0u8; 32];
getrandom::fill(&mut reset_key).map_err(other)?;
Expand Down Expand Up @@ -2259,7 +2298,7 @@
// ever will be, and the stream did not reach its FIN — a
// connection close must not read as one (issue #13,
// finding A2).
Err(ReadError::Blocked) => match conn_failure {

Check warning on line 2301 in endpoint/src/endpoint_impl.rs

View workflow job for this annotation

GitHub Actions / ci

manual implementation of `Option::map`
Some(err) => Some(Err(err)),
None => None,
},
Expand Down
12 changes: 6 additions & 6 deletions experiments/iroh-relay-ws/host/deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@
"lib": ["dom", "dom.iterable", "dom.asynciterable", "deno.ns"]
},
"imports": {
"@polyengine/runtime/embedder": "jsr:@polyengine/runtime@0.5.0/embedder",
"@polyengine/runtime/shim": "jsr:@polyengine/runtime@0.5.0/shim",
"@polyengine/protocol": "jsr:@polyengine/protocol@0.2.2",
"@polyengine/translator": "jsr:@polyengine/translator@0.5.0",
"@polyengine/wasi": "jsr:@polyengine/wasi@0.5.0",
"@polyengine/wasi/io": "jsr:@polyengine/wasi@0.5.0/io",
"@polyengine/runtime/embedder": "jsr:@polyengine/runtime@0.5.1/embedder",
"@polyengine/runtime/shim": "jsr:@polyengine/runtime@0.5.1/shim",
"@polyengine/protocol": "jsr:@polyengine/protocol@0.2.3",
"@polyengine/translator": "jsr:@polyengine/translator@0.5.1",
"@polyengine/wasi": "jsr:@polyengine/wasi@0.5.1",
"@polyengine/wasi/io": "jsr:@polyengine/wasi@0.5.1/io",
"@polymorph/webrtc-datachannels": "jsr:@polymorph/webrtc-datachannels@0.5.0",
"@polymorph/websocket": "jsr:@polymorph/websocket@0.5.0"
},
Expand Down
43 changes: 22 additions & 21 deletions experiments/iroh-relay-ws/host/deno.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 7 additions & 3 deletions host-polyengine/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ pinned module graph + the `node-datachannel` addon
`src/run-endpoint.ts` — the endpoint lifecycle (bind + identity through
idempotent teardown), the relay and WebRTC wires, the issue #10
concurrency rows, and the liveness/recovery rows (idle survival, relay
outage). Each scenario names its assertions where it lives; the exam's
outage, stalling-relay dial deadlines). Each scenario names its
assertions where it lives; the exam's
summary line is the inventory.

The exam retries the handshake-shaped scenarios a bounded number of
Expand All @@ -45,8 +46,11 @@ guest's and is latent on every host.
## The pin

polyengine and the sibling host modules arrive from JSR under caret
constraints on one minor line: the `@polyengine/{runtime,translator,wasi}@^0.5.0`
lockstep family, plus `@polyengine/protocol@^0.2.2` (versioned independently
constraints on one minor line: the `@polyengine/{runtime,translator,wasi}@^0.5.1`
lockstep family (0.5.1 is a floor, not a convenience: the endpoint's
dial timeouts drop in-flight import futures, which polyengine handles as
a prompt discard only from 0.5.1 — A23; under 0.5.0 the drop wedges the
store, polyengine#239), plus `@polyengine/protocol@^0.2.2` (versioned independently
of the lockstep family — the A22 host-ABI vocabulary line) and
`jsr:@polymorph/*@^0.5.0`. `deno.lock` pins the resolved versions and
carries integrity, enforced with `--frozen`. `@polyengine/translator` ships
Expand Down
6 changes: 3 additions & 3 deletions host-polyengine/deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@
"exclude": ["jsr:@polyengine/*", "jsr:@polymorph/*"]
},
"imports": {
"@polyengine/runtime/embedder": "jsr:@polyengine/runtime@^0.5.0/embedder",
"@polyengine/runtime/embedder": "jsr:@polyengine/runtime@^0.5.1/embedder",
"@polyengine/protocol": "jsr:@polyengine/protocol@^0.2.2",
"@polyengine/translator": "jsr:@polyengine/translator@^0.5.0",
"@polyengine/wasi": "jsr:@polyengine/wasi@^0.5.0",
"@polyengine/translator": "jsr:@polyengine/translator@^0.5.1",
"@polyengine/wasi": "jsr:@polyengine/wasi@^0.5.1",
"@polymorph/webcrypto": "jsr:@polymorph/webcrypto@^0.5.0",
"@polymorph/websocket": "jsr:@polymorph/websocket@^0.5.0",
"@polymorph/webrtc-datachannels": "jsr:@polymorph/webrtc-datachannels@^0.5.0"
Expand Down
41 changes: 21 additions & 20 deletions host-polyengine/deno.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading