2323use std:: {
2424 collections:: { HashMap , HashSet } ,
2525 fmt,
26+ sync:: Arc ,
2627 time:: { Duration , Instant } ,
2728} ;
2829
@@ -341,6 +342,9 @@ struct LegacyFallback {
341342pub struct PeerConnections {
342343 local_id : EndpointId ,
343344 conns : Mutex < HashMap < EndpointId , PeerConn > > ,
345+ /// Serializes connection replacement for one peer without making a slow
346+ /// dial block every other peer in the map.
347+ peer_gates : Mutex < HashMap < EndpointId , Arc < Mutex < ( ) > > > > ,
344348 legacy_notices : Mutex < HashSet < ( EndpointId , u64 ) > > ,
345349 legacy_fallbacks : Mutex < HashMap < ( EndpointId , u64 ) , LegacyFallback > > ,
346350 opened_tx : mpsc:: UnboundedSender < Connection > ,
@@ -353,6 +357,7 @@ impl PeerConnections {
353357 Self {
354358 local_id,
355359 conns : Mutex :: new ( HashMap :: new ( ) ) ,
360+ peer_gates : Mutex :: new ( HashMap :: new ( ) ) ,
356361 legacy_notices : Mutex :: new ( HashSet :: new ( ) ) ,
357362 legacy_fallbacks : Mutex :: new ( HashMap :: new ( ) ) ,
358363 opened_tx,
@@ -361,6 +366,15 @@ impl PeerConnections {
361366 }
362367 }
363368
369+ async fn peer_gate ( & self , peer : EndpointId ) -> Arc < Mutex < ( ) > > {
370+ self . peer_gates
371+ . lock ( )
372+ . await
373+ . entry ( peer)
374+ . or_insert_with ( || Arc :: new ( Mutex :: new ( ( ) ) ) )
375+ . clone ( )
376+ }
377+
364378 /// Open a mux stream to `peer_addr`'s exposure `protocol`, reusing the peer's
365379 /// shared connection (opening it if needed and replacing a dead cached one).
366380 /// A duplicate close gets a short bounded convergence window because a peer
@@ -628,6 +642,8 @@ impl PeerConnections {
628642 protocol : & str ,
629643 _activity : StreamActivity ,
630644 ) -> Result < Option < Connection > > {
645+ let peer_gate = self . peer_gate ( peer_addr. id ) . await ;
646+ let _peer_guard = peer_gate. lock ( ) . await ;
631647 let mut conns = self . conns . lock ( ) . await ;
632648 if let Some ( existing) = conns. get ( & peer_addr. id )
633649 && existing. generation == generation
@@ -652,6 +668,7 @@ impl PeerConnections {
652668 // retain the old canonical connection and reject the replacement.
653669 stale. connection . close ( 0u32 . into ( ) , STALE_GENERATION_REASON ) ;
654670 }
671+ drop ( conns) ;
655672 if self . mux_probe_deferred ( peer_addr. id , generation) . await {
656673 return Ok ( None ) ;
657674 }
@@ -671,6 +688,7 @@ impl PeerConnections {
671688 }
672689 } ;
673690 let remote_generation = Self :: exchange_generations ( & connection, generation) . await ?;
691+ let mut conns = self . conns . lock ( ) . await ;
674692 conns. insert (
675693 peer_addr. id ,
676694 PeerConn {
@@ -696,6 +714,8 @@ impl PeerConnections {
696714 stable_id : usize ,
697715 reason : & ' static [ u8 ] ,
698716 ) {
717+ let peer_gate = self . peer_gate ( peer) . await ;
718+ let _peer_guard = peer_gate. lock ( ) . await ;
699719 let mut connections = self . conns . lock ( ) . await ;
700720 if connections
701721 . get ( & peer)
@@ -710,6 +730,8 @@ impl PeerConnections {
710730
711731 /// Make one peer use a fresh multipath connection on its next stream.
712732 pub async fn redial ( & self , peer : EndpointId , reason : & [ u8 ] ) -> bool {
733+ let peer_gate = self . peer_gate ( peer) . await ;
734+ let _peer_guard = peer_gate. lock ( ) . await ;
713735 let removed = self . conns . lock ( ) . await . remove ( & peer) ;
714736 if let Some ( removed) = removed {
715737 removed. connection . close ( 0u32 . into ( ) , reason) ;
@@ -726,6 +748,8 @@ impl PeerConnections {
726748 remote_generation : u64 ,
727749 ) {
728750 let peer = connection. remote_id ( ) ;
751+ let peer_gate = self . peer_gate ( peer) . await ;
752+ let _peer_guard = peer_gate. lock ( ) . await ;
729753 let mut conns = self . conns . lock ( ) . await ;
730754 let replace = match conns. get ( & peer) {
731755 None => true ,
@@ -866,7 +890,6 @@ mod tests {
866890 endpoint:: presets,
867891 protocol:: { AcceptError , ProtocolHandler , Router } ,
868892 } ;
869- use std:: sync:: Arc ;
870893 use std:: sync:: atomic:: { AtomicUsize , Ordering } ;
871894
872895 const LEGACY_ECHO_ALPN : & [ u8 ] = b"fabric/test-legacy-echo/1" ;
@@ -1004,6 +1027,43 @@ mod tests {
10041027 }
10051028 }
10061029
1030+ #[ derive( Debug , Clone ) ]
1031+ struct BlockedGenerationMuxEcho {
1032+ entered : Arc < tokio:: sync:: Barrier > ,
1033+ release : Arc < tokio:: sync:: Notify > ,
1034+ }
1035+
1036+ impl ProtocolHandler for BlockedGenerationMuxEcho {
1037+ async fn accept ( & self , connection : Connection ) -> Result < ( ) , AcceptError > {
1038+ let result: Result < ( ) > = async {
1039+ let ( mut send, mut recv) = connection. accept_bi ( ) . await ?;
1040+ let _remote_generation = recv. read_u64 ( ) . await ?;
1041+ self . entered . wait ( ) . await ;
1042+ self . release . notified ( ) . await ;
1043+ send. write_u64 ( 0 ) . await ?;
1044+ send. finish ( ) ?;
1045+
1046+ loop {
1047+ let ( mut send, mut recv) = match connection. accept_bi ( ) . await {
1048+ Ok ( pair) => pair,
1049+ Err ( _) => break ,
1050+ } ;
1051+ tokio:: spawn ( async move {
1052+ if MuxStreamHeader :: read ( & mut recv) . await . is_ok ( ) {
1053+ let _ = write_ready ( & mut send) . await ;
1054+ let _ = tokio:: io:: copy ( & mut recv, & mut send) . await ;
1055+ let _ = send. finish ( ) ;
1056+ }
1057+ } ) ;
1058+ }
1059+ Ok ( ( ) )
1060+ }
1061+ . await ;
1062+ result
1063+ . map_err ( |error| AcceptError :: from_err ( std:: io:: Error :: other ( format ! ( "{error:#}" ) ) ) )
1064+ }
1065+ }
1066+
10071067 async fn finish_streams_then_echo ( connection : Connection , failures : usize ) {
10081068 let mut streams = 0usize ;
10091069 loop {
@@ -1027,6 +1087,98 @@ mod tests {
10271087 }
10281088 }
10291089
1090+ /// A slow connection to one peer must not delay an existing connection to
1091+ /// another peer. Production showed a 2-second delay every 20 seconds when
1092+ /// the health loop probed an absent peer beside a healthy peer.
1093+ #[ tokio:: test( flavor = "multi_thread" , worker_threads = 4 ) ]
1094+ async fn one_slow_peer_open_does_not_block_another_peer ( ) -> Result < ( ) > {
1095+ let client = Endpoint :: builder ( presets:: N0 ) . bind ( ) . await ?;
1096+ let ( opened_tx, _opened_rx) = mpsc:: unbounded_channel ( ) ;
1097+ let manager = Arc :: new ( PeerConnections :: new ( client. id ( ) , opened_tx) ) ;
1098+
1099+ let healthy = Router :: builder (
1100+ Endpoint :: builder ( presets:: N0 )
1101+ . alpns ( vec ! [ MUX_ALPN . to_vec( ) ] )
1102+ . bind ( )
1103+ . await ?,
1104+ )
1105+ . accept (
1106+ MUX_ALPN ,
1107+ MuxEcho {
1108+ connections : Arc :: new ( AtomicUsize :: new ( 0 ) ) ,
1109+ headers : Arc :: new ( Mutex :: new ( Vec :: new ( ) ) ) ,
1110+ } ,
1111+ )
1112+ . spawn ( ) ;
1113+ let entered = Arc :: new ( tokio:: sync:: Barrier :: new ( 2 ) ) ;
1114+ let release = Arc :: new ( tokio:: sync:: Notify :: new ( ) ) ;
1115+ let slow = Router :: builder (
1116+ Endpoint :: builder ( presets:: N0 )
1117+ . alpns ( vec ! [ MUX_ALPN . to_vec( ) ] )
1118+ . bind ( )
1119+ . await ?,
1120+ )
1121+ . accept (
1122+ MUX_ALPN ,
1123+ BlockedGenerationMuxEcho {
1124+ entered : entered. clone ( ) ,
1125+ release : release. clone ( ) ,
1126+ } ,
1127+ )
1128+ . spawn ( ) ;
1129+ healthy. endpoint ( ) . online ( ) . await ;
1130+ slow. endpoint ( ) . online ( ) . await ;
1131+
1132+ let first = manager
1133+ . open_mux_stream (
1134+ & client,
1135+ 0 ,
1136+ & healthy. endpoint ( ) . addr ( ) ,
1137+ "first" ,
1138+ StreamActivity :: Probe ,
1139+ )
1140+ . await ?;
1141+ drop ( first) ;
1142+
1143+ let slow_manager = manager. clone ( ) ;
1144+ let slow_client = client. clone ( ) ;
1145+ let slow_addr = slow. endpoint ( ) . addr ( ) ;
1146+ let slow_open = tokio:: spawn ( async move {
1147+ slow_manager
1148+ . open_mux_stream ( & slow_client, 0 , & slow_addr, "slow" , StreamActivity :: Probe )
1149+ . await
1150+ } ) ;
1151+ tokio:: time:: timeout ( Duration :: from_secs ( 3 ) , entered. wait ( ) )
1152+ . await
1153+ . context ( "the slow peer did not reach generation exchange" ) ?;
1154+
1155+ let healthy_open = tokio:: time:: timeout (
1156+ Duration :: from_millis ( 250 ) ,
1157+ manager. open_mux_stream (
1158+ & client,
1159+ 0 ,
1160+ & healthy. endpoint ( ) . addr ( ) ,
1161+ "healthy" ,
1162+ StreamActivity :: Probe ,
1163+ ) ,
1164+ )
1165+ . await ;
1166+ release. notify_one ( ) ;
1167+ let slow_stream = slow_open. await ??;
1168+ drop ( slow_stream) ;
1169+
1170+ assert ! (
1171+ healthy_open. is_ok( ) ,
1172+ "a slow open to one peer blocked an existing connection to another peer"
1173+ ) ;
1174+ healthy_open. expect ( "checked timeout" ) ?;
1175+
1176+ slow. shutdown ( ) . await ?;
1177+ healthy. shutdown ( ) . await ?;
1178+ client. close ( ) . await ;
1179+ Ok ( ( ) )
1180+ }
1181+
10301182 async fn assert_stream_failure_recovery (
10311183 failures : usize ,
10321184 expect_same_connection : bool ,
0 commit comments