Skip to content

Commit 295a715

Browse files
authored
fix(iroh): Correctly abandon paths when new are opened with worse RTT (#4296)
## Description Locally, `test_paths_watcher` and `endpoint_two_direct_add_relay` fairly consistently failed for me. I dug into `test_paths_watcher`, this was what was going on: 1. A new connection is established via IP path 2. The test waits for both relay and IP path to be established with a 1s timeout 3. The connection immediately tries to add a relay path after it was established via IP path, but it can't yet because of missing remote CIDs, so it schedules a later path open for the relay path. 4. <lots and lots of path open events, many many new paths opened and immedaitely closed> 5. Scheduled path open for the relay path triggers, but it fails because MaxPathIdReached. Unlike RemoteCidsExhausted, it doesn't re-schedule this path open. 6. 1s timeout triggers The weird thing was that the max path ID limit wasn't increased. This was because the hole-punched paths that were opened were never cleaned up - no path was ever abandoned. Digging deeper I found out that our logic for abandoning redundant IP paths wasn't triggered when the newly hole-punched path wasn't better than our current paths. This PR fixes that. During investigation I also found out that we would never re-try opening the relay path in case you get a `MaxPathIdReached` error, which seems bad, so now we explicitly handle this case as well. ## Breaking Changes None ## Notes & open questions Arguably, we could also decide to not match on the particular error that causes us to not be able to open a path and always re-schedule path opening on error? ## Change checklist <!-- Remove any that are not relevant. --> - [x] Self-review.
1 parent ee8b6a3 commit 295a715

2 files changed

Lines changed: 17 additions & 9 deletions

File tree

iroh/src/endpoint/connection.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1503,7 +1503,7 @@ mod tests {
15031503
async fn test_paths_watcher() -> Result {
15041504
const ALPN: &[u8] = b"test";
15051505
let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0u64);
1506-
let (relay_map, _relay_map, _guard) = run_relay_server().await?;
1506+
let (relay_map, _relay_url, _guard) = run_relay_server().await?;
15071507
let server = Endpoint::builder(presets::Minimal)
15081508
.relay_mode(RelayMode::Custom(relay_map.clone()))
15091509
.secret_key(SecretKey::from_bytes(&rng.random()))
@@ -1532,7 +1532,7 @@ mod tests {
15321532
let mut paths_client = conn_client.paths_stream();
15331533
let mut paths_server = conn_server.paths_stream();
15341534

1535-
/// Advances the path stream until at least one IP and one relay paths are available.
1535+
/// Advances the path stream until at least one IP and one relay path is available.
15361536
///
15371537
/// Panics if the path stream finishes before that happens.
15381538
async fn wait_for_paths(mut stream: impl Send + Unpin + Stream<Item = PathList<'_>>) {

iroh/src/socket/remote_map/remote_state.rs

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -666,10 +666,11 @@ impl RemoteStateActor {
666666
network_path = %addr,
667667
prev_network_path = prev_remote.map(|p| format!("{p}")).unwrap_or("None".to_string()),
668668
);
669-
self.apply_selected_change(&addr);
670669
} else {
671670
trace!(?current_path, "keeping current path");
672671
}
672+
673+
self.apply_selected_path();
673674
}
674675

675676
/// Propagates a change of [`State::selected_path`] to noq.
@@ -679,15 +680,21 @@ impl RemoteStateActor {
679680
/// - Sets all non-selected paths to [`PathStatus::Backup`]
680681
/// - Opens the selected path if it does not exist on the connection
681682
/// - Sets the selected path to [`PathStatus::Available`]
682-
fn apply_selected_change(&mut self, selected: &transports::FourTuple) {
683+
fn apply_selected_path(&mut self) {
684+
let Some(selected) = self.state.selected_path.clone() else {
685+
// We can't open the selected path on all paths if we don't have one yet.
686+
// And we can't close all "unselected" paths either, because we don't know which one is selected.
687+
return;
688+
};
689+
683690
for (conn_id, conn_state) in self.connections.iter() {
684691
let Some(conn) = conn_state.handle.upgrade() else {
685692
continue;
686693
};
687694

688695
// Open path if it doesn't exist yet.
689696
self.state
690-
.open_path_on_conn(*conn_id, conn_state, &conn, selected);
697+
.open_path_on_conn(*conn_id, conn_state, &conn, &selected);
691698

692699
for (path_id, path_remote) in conn_state.paths.iter() {
693700
let Some(path) = conn.path(*path_id) else {
@@ -701,7 +708,7 @@ impl RemoteStateActor {
701708
// and racing to abandon the last one.
702709
if conn.side().is_client()
703710
&& path_remote.is_ip()
704-
&& path_remote != selected
711+
&& path_remote != &selected
705712
&& conn_state.paths.values().filter(|a| a.is_ip()).count() > 1
706713
{
707714
trace!(?path_remote, %conn_id, %path_id, "closing direct path");
@@ -725,7 +732,7 @@ impl RemoteStateActor {
725732
}
726733

727734
// Record the new selected path in the path watcher.
728-
conn_state.path_state.record_selected(selected);
735+
conn_state.path_state.record_selected(&selected);
729736
}
730737
}
731738

@@ -1045,11 +1052,12 @@ impl State {
10451052
None => {
10461053
let ret = now_or_never(fut);
10471054
match ret {
1048-
Some(Err(PathError::RemoteCidsExhausted)) => {
1055+
Some(Err(PathError::RemoteCidsExhausted))
1056+
| Some(Err(PathError::MaxPathIdReached)) => {
10491057
self.scheduled_open_path =
10501058
Some(Instant::now() + Duration::from_millis(333));
10511059
self.pending_open_paths.push_back(open_addr.clone());
1052-
trace!(?open_addr, "scheduling open_path");
1060+
trace!(?open_addr, ?ret, "scheduling open_path");
10531061
}
10541062
_ => warn!(?ret, "Opening path failed"),
10551063
}

0 commit comments

Comments
 (0)