@@ -73,6 +73,7 @@ pub struct EndpointConfig {
7373const IDLE_TIMEOUT_MS : u32 = 30000 ;
7474const KEEP_ALIVE_MS : u64 = 500 ;
7575const HAPPY_EYEBALLS_DELAY_MS : u64 = 250 ;
76+ const HAPPY_EYEBALLS_MAX_IN_FLIGHT : usize = 10 ;
7677pub const DEFAULT_EXPECTED_RTT_MS : u64 = 100 ;
7778
7879#[ derive( Clone , Debug ) ]
@@ -693,6 +694,7 @@ pub async fn connect(
693694 . to_socket_addrs ( )
694695 . internal_with ( || format ! ( "remote {remote_url} is invalid" ) ) ?
695696 . collect ( ) ;
697+ let remote_addrs = interleave_socket_addrs ( remote_addrs) ;
696698 let server_name = config. sni_override . as_deref ( ) . unwrap_or ( host. as_str ( ) ) ;
697699
698700 let validate_certificate = url. scheme ( ) . ends_with ( "s" ) ;
@@ -772,6 +774,38 @@ pub async fn connect(
772774 Err ( ProtocolError :: internal ( format ! ( "connect: {remote_url}" ) ) )
773775}
774776
777+ fn interleave_socket_addrs ( remote_addrs : Vec < SocketAddr > ) -> Vec < SocketAddr > {
778+ let Some ( first) = remote_addrs. first ( ) else {
779+ return remote_addrs;
780+ } ;
781+ let prefer_ipv6 = first. is_ipv6 ( ) ;
782+ let ( preferred, fallback) : ( Vec < _ > , Vec < _ > ) = remote_addrs
783+ . into_iter ( )
784+ . partition ( |addr| addr. is_ipv6 ( ) == prefer_ipv6) ;
785+ let mut preferred = preferred. into_iter ( ) ;
786+ let mut fallback = fallback. into_iter ( ) ;
787+ let mut interleaved = Vec :: with_capacity ( preferred. len ( ) + fallback. len ( ) ) ;
788+
789+ loop {
790+ match preferred. next ( ) {
791+ Some ( addr) => interleaved. push ( addr) ,
792+ None => {
793+ interleaved. extend ( fallback) ;
794+ break ;
795+ }
796+ }
797+ match fallback. next ( ) {
798+ Some ( addr) => interleaved. push ( addr) ,
799+ None => {
800+ interleaved. extend ( preferred) ;
801+ break ;
802+ }
803+ }
804+ }
805+
806+ interleaved
807+ }
808+
775809async fn connect_happy_eyeballs < T , F , Fut > (
776810 remote_addrs : Vec < SocketAddr > ,
777811 attempt_delay : Duration ,
@@ -802,6 +836,8 @@ where
802836 tokio:: select! {
803837 result = attempts. next( ) , if !attempts. is_empty( ) => {
804838 if let Some ( Some ( connection) ) = result {
839+ // Dropping `attempts` cancels the losing Quinn handshakes because each
840+ // production future owns its `Connecting` and `Endpoint`.
805841 return Some ( connection) ;
806842 }
807843 if attempts. is_empty( ) {
@@ -813,7 +849,7 @@ where
813849 delay. as_mut( ) . reset( tokio:: time:: Instant :: now( ) + attempt_delay) ;
814850 }
815851 }
816- _ = & mut delay => {
852+ _ = & mut delay, if attempts . len ( ) < HAPPY_EYEBALLS_MAX_IN_FLIGHT => {
817853 let Some ( addr) = next_addr. take( ) else {
818854 continue ;
819855 } ;
@@ -1244,8 +1280,12 @@ mod tests {
12441280 use std:: time:: Duration ;
12451281
12461282 use parking_lot:: Mutex ;
1283+ use tokio:: sync:: Semaphore ;
1284+ use tokio:: sync:: mpsc;
12471285
1286+ use super :: HAPPY_EYEBALLS_MAX_IN_FLIGHT ;
12481287 use super :: connect_happy_eyeballs;
1288+ use super :: interleave_socket_addrs;
12491289
12501290 fn ipv6_addr ( ) -> SocketAddr {
12511291 "[::1]:41337" . parse ( ) . unwrap ( )
@@ -1255,6 +1295,41 @@ mod tests {
12551295 "127.0.0.1:41337" . parse ( ) . unwrap ( )
12561296 }
12571297
1298+ #[ test]
1299+ fn happy_eyeballs_interleaves_ipv6_first_addresses ( ) {
1300+ let ipv6_second = "[::2]:41337" . parse ( ) . unwrap ( ) ;
1301+ let ipv6_third = "[::3]:41337" . parse ( ) . unwrap ( ) ;
1302+ let ipv4_second = "127.0.0.2:41337" . parse ( ) . unwrap ( ) ;
1303+
1304+ assert_eq ! (
1305+ interleave_socket_addrs( vec![
1306+ ipv6_addr( ) ,
1307+ ipv6_second,
1308+ ipv6_third,
1309+ ipv4_addr( ) ,
1310+ ipv4_second,
1311+ ] ) ,
1312+ vec![
1313+ ipv6_addr( ) ,
1314+ ipv4_addr( ) ,
1315+ ipv6_second,
1316+ ipv4_second,
1317+ ipv6_third,
1318+ ]
1319+ ) ;
1320+ }
1321+
1322+ #[ test]
1323+ fn happy_eyeballs_interleaves_ipv4_first_addresses ( ) {
1324+ let ipv4_second = "127.0.0.2:41337" . parse ( ) . unwrap ( ) ;
1325+ let ipv6_second = "[::2]:41337" . parse ( ) . unwrap ( ) ;
1326+
1327+ assert_eq ! (
1328+ interleave_socket_addrs( vec![ ipv4_addr( ) , ipv4_second, ipv6_addr( ) , ipv6_second, ] ) ,
1329+ vec![ ipv4_addr( ) , ipv6_addr( ) , ipv4_second, ipv6_second]
1330+ ) ;
1331+ }
1332+
12581333 #[ tokio:: test]
12591334 async fn happy_eyeballs_starts_fallback_while_first_attempt_is_stalled ( ) {
12601335 let attempts = Arc :: new ( Mutex :: new ( Vec :: new ( ) ) ) ;
@@ -1334,4 +1409,52 @@ mod tests {
13341409
13351410 assert_eq ! ( result, None ) ;
13361411 }
1412+
1413+ #[ tokio:: test]
1414+ async fn happy_eyeballs_bounds_in_flight_attempts ( ) {
1415+ let remote_addrs: Vec < _ > = ( 1 ..=HAPPY_EYEBALLS_MAX_IN_FLIGHT + 1 )
1416+ . map ( |port| SocketAddr :: new ( ipv6_addr ( ) . ip ( ) , port as u16 ) )
1417+ . collect ( ) ;
1418+ let release = Arc :: new ( Semaphore :: new ( 0 ) ) ;
1419+ let attempt_release = release. clone ( ) ;
1420+ let ( started_tx, mut started_rx) = mpsc:: unbounded_channel ( ) ;
1421+
1422+ let task = lore_base:: lore_spawn!( connect_happy_eyeballs(
1423+ remote_addrs. clone( ) ,
1424+ Duration :: from_millis( 1 ) ,
1425+ move |addr| {
1426+ started_tx. send( addr) . unwrap( ) ;
1427+ let attempt_release = attempt_release. clone( ) ;
1428+ async move {
1429+ attempt_release. acquire( ) . await . unwrap( ) . forget( ) ;
1430+ None :: <SocketAddr >
1431+ }
1432+ } ,
1433+ ) ) ;
1434+
1435+ for expected in remote_addrs. iter ( ) . take ( HAPPY_EYEBALLS_MAX_IN_FLIGHT ) {
1436+ assert_eq ! (
1437+ tokio:: time:: timeout( Duration :: from_secs( 2 ) , started_rx. recv( ) )
1438+ . await
1439+ . expect( "attempt should start" ) ,
1440+ Some ( * expected)
1441+ ) ;
1442+ }
1443+ assert ! (
1444+ tokio:: time:: timeout( Duration :: from_millis( 50 ) , started_rx. recv( ) )
1445+ . await
1446+ . is_err( ) ,
1447+ "attempts above the in-flight limit should remain queued"
1448+ ) ;
1449+
1450+ release. add_permits ( 1 ) ;
1451+ assert_eq ! (
1452+ tokio:: time:: timeout( Duration :: from_secs( 2 ) , started_rx. recv( ) )
1453+ . await
1454+ . expect( "queued attempt should start when a slot opens" ) ,
1455+ Some ( remote_addrs[ HAPPY_EYEBALLS_MAX_IN_FLIGHT ] )
1456+ ) ;
1457+
1458+ task. abort ( ) ;
1459+ }
13371460}
0 commit comments