fix(router): establish mux legs asynchronously so a dial serves on its primary immediately - #4209
Merged
Merged
Conversation
…s primary immediately The Dial path called establishMuxRoutes synchronously in finishDial, so it blocked until up to initialForegroundMux (16) aux legs finished Phase-1 planning. Each plan hits the route-finder, and when the RF times out the whole dial stalled ~90s before the app could bind and serve — even though the primary route (leg 0) is ready in ~1s. Now the fast, non-dialing work stays synchronous (compute muxTarget + same-LAN/control-plane guards, wire the leg-change / self-heal / rotation callbacks) and the slow leg establishment — establishMuxRoutes, then applyDistribution, then maybeSelfHeal — runs in a goroutine on a fresh 5-minute background context (the dial ctx may be canceled once Dial returns). Dial returns as soon as the primary route group is up; the mux legs fill in behind it. The no-mux branch keeps applyDistribution synchronous.
0pcom
added a commit
that referenced
this pull request
Aug 26, 2026
…rsification (#4216) Keep N healthy tunnels in the multi-tunnel skysocks client so the aggregation width does not bleed down over time. Previously the keepalive loop only SKIPPED a dead tunnel (pickSession routes around it); it never replaced one, so a long-lived --tunnels N client decayed toward a single tunnel and lost the bandwidth aggregation. The Client now stores the target N and, when a tunnel dies and the live count falls below N (but at least one survives), re-dials a fresh DISJOINT replacement via a callback the app wires in (SetTunnelRedial closes over the same diversify=true dialServer path the initial extra tunnels use, so the visor steers the replacement off the survivors' first-hop transports, #4214). Re-dial is bounded: a single in-flight attempt (atomic CAS guard) and a back-off after consecutive failures until the next tunnel death re-arms it, so a persistently unreachable exit does not spin. N==1 never re-dials — its lone tunnel's death is total collapse, still owned by the app's --reconnect runCycle — so the default path is byte-identical. Sequential diversification is reliable without a router change or an added delay: the visor registers each tunnel's primary route group (with its first-hop transport) into rgsNs synchronously inside saveRouteGroupRules, before the dial RPC returns; #4209 defers only auxiliary mux legs, which skysocks tunnels never request. Because the extra-tunnel loop dials sequentially, tunnel i is always visible to tunnel i+1's sibling-exclusion scan. Documented at both sites. Throughput-based eviction of a slow-but-alive tunnel (the "drop the underperforming" half of RFC step 4) is deferred: it needs the gigabit validation rig to tune the slow-leg threshold. This change is liveness-only.
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.
The Dial path called
establishMuxRoutessynchronously infinishDial, so it blocked until up toinitialForegroundMux(16) aux legs finished Phase-1 planning. Each plan hits the route-finder, and when the RF times out the whole dial stalled ~90s before the app (e.g. skysocks-client) could bind and serve — even though the primary route (leg 0) is ready in ~1s.This runs the slow leg establishment in the background. The fast, non-dialing work stays synchronous: computing
muxTarget+ the same-LAN/control-plane no-mux guards, and wiring the leg-change / self-heal / rotation callbacks (they only install callbacks, no dialing). The slow part —establishMuxRoutes, thenapplyDistribution, thenmaybeSelfHeal— moves into a goroutine on a fresh 5-minute background context (the dial's ctx may be canceled once Dial returns). Ordering is preserved so the distribution rebuild still sees every leg.Dial now returns as soon as the primary route group is up; the mux legs fill in behind it. The no-mux branch keeps
applyDistributionsynchronous. Full router suite stays green.