@@ -1099,25 +1099,32 @@ impl DialFailureCache {
10991099}
11001100
11011101/// ADR-011 self-heal: when a newer authoritative `PublishAddressSet` from
1102- /// `publisher` is applied, clear any stale dial-failure suppression for its
1103- /// freshly published socket addresses. A recovered address — for example a relay
1104- /// that now hands a reconnecting peer back its previous stable port — is
1105- /// otherwise kept suppressed for up to [`DIAL_FAILURE_CACHE_TTL`] even though the
1106- /// owner has just re-attested it. The exemption it grants is keyed by
1107- /// `(publisher, socket)` so only a dial *to that publisher* benefits. Returns the
1108- /// number of addresses cleared; a no-op when the publish was not applied (stale
1109- /// or duplicate sequence).
1102+ /// `publisher` is applied, clear stale dial-failure suppression only for socket
1103+ /// addresses absent from `previous_addresses`.
1104+ ///
1105+ /// This immediately retries a genuinely withdrawn then recovered address
1106+ /// without treating a sequence-only refresh of an unchanged bad relay as proof
1107+ /// of recovery. The exemption is keyed by `(publisher, socket)` so only a dial
1108+ /// *to that publisher* benefits. Returns the number of addresses cleared; a
1109+ /// no-op when the publish was not applied (stale or duplicate sequence).
11101110fn clear_dial_failures_for_published (
11111111 cache : & DialFailureCache ,
11121112 publisher : & PeerId ,
11131113 applied : bool ,
1114+ previous_addresses : & [ ( crate :: MultiAddr , AddressType ) ] ,
11141115 addresses : & [ ( crate :: MultiAddr , AddressType ) ] ,
11151116) -> usize {
11161117 if !applied {
11171118 return 0 ;
11181119 }
11191120 let mut cleared = 0 ;
11201121 for ( addr, _ty) in addresses {
1122+ if previous_addresses
1123+ . iter ( )
1124+ . any ( |( previous, _) | previous == addr)
1125+ {
1126+ continue ;
1127+ }
11211128 if let Some ( socket_addr) = addr. dialable_socket_addr ( ) {
11221129 // Clears this address's own failure and grants (publisher, socket) a
11231130 // short exemption from IP suppression — but never lifts IP-level relay
@@ -3574,7 +3581,7 @@ impl DhtNetworkManager {
35743581 /// broadcast and translate the shared outcome back into a
35753582 /// caller-facing [`P2PError`] — they do not duplicate the
35763583 /// owner's side effects.
3577- async fn ensure_peer_channel (
3584+ pub ( crate ) async fn ensure_peer_channel (
35783585 & self ,
35793586 peer_id : & PeerId ,
35803587 candidates : & [ ( MultiAddr , AddressType ) ] ,
@@ -4468,13 +4475,15 @@ impl DhtNetworkManager {
44684475 ) ;
44694476 }
44704477 let dht = self . dht . read ( ) . await ;
4478+ let previous_addresses = dht. get_node_addresses_typed ( authenticated_sender) . await ;
44714479 let applied = dht
44724480 . replace_node_addresses ( authenticated_sender, filtered_addresses. clone ( ) , * seq)
44734481 . await ;
4474- // ADR-011: a newer authoritative address set just landed — lift
4475- // stale dial-failure suppression for its addresses so a
4476- // self-healed (e.g. reclaimed stable relay) address is retried
4477- // immediately instead of staying suppressed for the cache TTL.
4482+ // ADR-011: a newer authoritative address set just landed. Lift
4483+ // stale dial-failure suppression only for addresses that were
4484+ // absent from the previous record and have genuinely
4485+ // reappeared (for example a reclaimed stable relay). Refreshing
4486+ // an unchanged failed address must not create a redial loop.
44784487 //
44794488 // Race guard: only exempt addresses that are STILL the peer's
44804489 // current record after the apply (read back under the same lock
@@ -4493,6 +4502,7 @@ impl DhtNetworkManager {
44934502 self . dial_failure_cache . as_ref ( ) ,
44944503 authenticated_sender,
44954504 true ,
4505+ & previous_addresses,
44964506 & still_current,
44974507 )
44984508 } else {
@@ -5664,41 +5674,64 @@ impl DhtNetworkManager {
56645674 & self ,
56655675 typed_addresses : Vec < ( crate :: MultiAddr , AddressType ) > ,
56665676 peers : & [ DHTNode ] ,
5667- ) {
5677+ ) -> Vec < PeerId > {
56685678 let seq = Self :: next_publish_seq ( ) ;
56695679 let op = DhtNetworkOperation :: PublishAddressSet {
56705680 seq,
56715681 addresses : typed_addresses. clone ( ) ,
56725682 } ;
5683+ let mut confirmed = Vec :: new ( ) ;
5684+ let mut publishes = FuturesUnordered :: new ( ) ;
56735685 for peer in peers {
56745686 if peer. peer_id == self . config . peer_id {
56755687 continue ; // Skip self
56765688 }
56775689 // Pass the peer's typed addresses through directly so
56785690 // send_dht_request avoids a redundant routing-table read for
56795691 // a peer we already have in hand.
5692+ let peer_id = peer. peer_id ;
56805693 let peer_typed = peer. typed_addresses ( ) ;
5681- match self
5682- . send_dht_request ( & peer. peer_id , op. clone ( ) , Some ( & peer_typed) )
5683- . await
5684- {
5685- Ok ( _) => {
5694+ let op = op. clone ( ) ;
5695+ publishes. push ( async move {
5696+ (
5697+ peer_id,
5698+ self . send_dht_request ( & peer_id, op, Some ( & peer_typed) ) . await ,
5699+ )
5700+ } ) ;
5701+ }
5702+
5703+ // A withdrawal must not spend one full request timeout per unavailable
5704+ // peer while the rest of the network continues dialing the old relay.
5705+ // Fan the full replacement out concurrently and retain the exact
5706+ // acknowledgers so the driver can retry only missing replicas.
5707+ while let Some ( ( peer_id, result) ) = publishes. next ( ) . await {
5708+ match result {
5709+ Ok ( DhtNetworkResult :: PublishAddressAck ) => {
5710+ confirmed. push ( peer_id) ;
56865711 debug ! (
5687- peer = %peer . peer_id. to_hex( ) ,
5712+ peer = %peer_id. to_hex( ) ,
56885713 addrs = typed_addresses. len( ) ,
56895714 seq,
56905715 "published address set to peer" ,
56915716 ) ;
56925717 }
5718+ Ok ( other) => {
5719+ debug ! (
5720+ peer = %peer_id. to_hex( ) ,
5721+ result = ?other,
5722+ "Peer returned an unexpected address publication response"
5723+ ) ;
5724+ }
56935725 Err ( e) => {
56945726 debug ! (
56955727 "Failed to publish address set to peer {}: {}" ,
5696- peer . peer_id. to_hex( ) ,
5728+ peer_id. to_hex( ) ,
56975729 e
56985730 ) ;
56995731 }
57005732 }
57015733 }
5734+ confirmed
57025735 }
57035736
57045737 /// Generate the next monotonic publish sequence number.
@@ -7257,7 +7290,7 @@ mod tests {
72577290
72587291 // A stale/duplicate publish (not applied) must NOT lift suppression.
72597292 assert_eq ! (
7260- clear_dial_failures_for_published( & cache, & peer, false , & addrs) ,
7293+ clear_dial_failures_for_published( & cache, & peer, false , & [ ] , & addrs) ,
72617294 0
72627295 ) ;
72637296 assert ! ( cache. is_failed( & relay_sa, AddressType :: Relay ) ) ;
@@ -7266,13 +7299,53 @@ mod tests {
72667299 // A newer applied publish clears the per-address failures (neither IP is
72677300 // over the suppression threshold here, so both become dialable).
72687301 assert_eq ! (
7269- clear_dial_failures_for_published( & cache, & peer, true , & addrs) ,
7302+ clear_dial_failures_for_published( & cache, & peer, true , & [ ] , & addrs) ,
72707303 2
72717304 ) ;
72727305 assert ! ( !cache. is_failed( & relay_sa, AddressType :: Relay ) ) ;
72737306 assert ! ( !cache. is_failed( & direct_sa, AddressType :: Direct ) ) ;
72747307 }
72757308
7309+ #[ test]
7310+ fn repeated_unchanged_publish_does_not_clear_fresh_dial_failure ( ) {
7311+ let cache = DialFailureCache :: new ( ) ;
7312+ let peer = PeerId :: from_bytes ( [ 1 ; 32 ] ) ;
7313+ let relay = crate :: MultiAddr :: quic ( sock ( "203.0.113.7:9000" ) ) ;
7314+ let relay_sa = relay. dialable_socket_addr ( ) . expect ( "dialable" ) ;
7315+ let addrs = vec ! [ ( relay, AddressType :: Relay ) ] ;
7316+
7317+ cache. record_failure ( relay_sa, AddressType :: Relay ) ;
7318+ assert_eq ! (
7319+ clear_dial_failures_for_published( & cache, & peer, true , & addrs, & addrs) ,
7320+ 0
7321+ ) ;
7322+ assert ! (
7323+ cache. is_failed_for_dial( & peer, & relay_sa, AddressType :: Relay ) ,
7324+ "refreshing an unchanged bad relay must not create a retry loop"
7325+ ) ;
7326+ }
7327+
7328+ #[ test]
7329+ fn relay_reappearing_after_withdrawal_clears_old_dial_failure ( ) {
7330+ let cache = DialFailureCache :: new ( ) ;
7331+ let peer = PeerId :: from_bytes ( [ 1 ; 32 ] ) ;
7332+ let relay = crate :: MultiAddr :: quic ( sock ( "203.0.113.7:9000" ) ) ;
7333+ let direct = crate :: MultiAddr :: quic ( sock ( "203.0.113.8:9000" ) ) ;
7334+ let relay_sa = relay. dialable_socket_addr ( ) . expect ( "dialable" ) ;
7335+ let previous = vec ! [ ( direct. clone( ) , AddressType :: Direct ) ] ;
7336+ let current = vec ! [ ( relay, AddressType :: Relay ) , ( direct, AddressType :: Direct ) ] ;
7337+
7338+ cache. record_failure ( relay_sa, AddressType :: Relay ) ;
7339+ assert_eq ! (
7340+ clear_dial_failures_for_published( & cache, & peer, true , & previous, & current) ,
7341+ 1
7342+ ) ;
7343+ assert ! (
7344+ !cache. is_failed_for_dial( & peer, & relay_sa, AddressType :: Relay ) ,
7345+ "a genuinely withdrawn then re-acquired relay should be retried"
7346+ ) ;
7347+ }
7348+
72767349 #[ test]
72777350 fn publish_self_heal_keeps_ip_tier_suppression_for_others ( ) {
72787351 let cache = DialFailureCache :: new ( ) ;
@@ -7293,7 +7366,7 @@ mod tests {
72937366 // The owner re-attests one of those addresses via an applied publish.
72947367 let addrs = vec ! [ ( crate :: MultiAddr :: quic( socks[ 0 ] ) , AddressType :: Relay ) ] ;
72957368 assert_eq ! (
7296- clear_dial_failures_for_published( & cache, & peer, true , & addrs) ,
7369+ clear_dial_failures_for_published( & cache, & peer, true , & [ ] , & addrs) ,
72977370 1
72987371 ) ;
72997372
@@ -7334,7 +7407,7 @@ mod tests {
73347407 // Its owner reclaims and re-attests it via an applied publish.
73357408 let addrs = vec ! [ ( crate :: MultiAddr :: quic( socks[ 0 ] ) , AddressType :: Relay ) ] ;
73367409 assert_eq ! (
7337- clear_dial_failures_for_published( & cache, & owner, true , & addrs) ,
7410+ clear_dial_failures_for_published( & cache, & owner, true , & [ ] , & addrs) ,
73387411 1
73397412 ) ;
73407413
0 commit comments