@@ -26,6 +26,7 @@ use libp2p::{
2626} ;
2727use libp2p_swarm_derive:: NetworkBehaviour ;
2828use serde:: { Deserialize , Serialize } ;
29+ use tokio_util:: time:: DelayQueue ;
2930
3031use sqd_contract_client:: { Client as ContractClient , NetworkNodes } ;
3132
@@ -80,6 +81,9 @@ pub struct BaseConfig {
8081 pub addr_cache_size : NonZeroUsize ,
8182 /// Maximum number of concurrent Kademlia lookups for worker discovery (default: 20)
8283 pub max_concurrent_lookups : usize ,
84+ /// Cooldown before re-dialing a worker after its connection closes (default: 30 sec).
85+ /// Prevents a tight dial→refuse→redial loop against workers that reject this peer.
86+ pub reconnect_cooldown : Duration ,
8387}
8488
8589impl BaseConfig {
@@ -94,6 +98,7 @@ impl BaseConfig {
9498 let addr_cache_size = NonZeroUsize :: new ( parse_env_var ( "ADDR_CACHE_SIZE" , 1024 ) )
9599 . expect ( "addr_cache_size should be > 0" ) ;
96100 let max_concurrent_lookups = parse_env_var ( "MAX_CONCURRENT_LOOKUPS" , 20 ) ;
101+ let reconnect_cooldown = Duration :: from_secs ( parse_env_var ( "RECONNECT_COOLDOWN_SEC" , 30 ) ) ;
97102 Self {
98103 onchain_update_interval,
99104 autonat_timeout,
@@ -103,6 +108,7 @@ impl BaseConfig {
103108 max_pubsub_msg_size,
104109 addr_cache_size,
105110 max_concurrent_lookups,
111+ reconnect_cooldown,
106112 }
107113 }
108114}
@@ -121,6 +127,12 @@ pub struct BaseBehaviour {
121127 maintain_worker_connections : bool ,
122128 pending_lookups : VecDeque < PeerId > ,
123129 max_concurrent_lookups : usize ,
130+ /// Flat cooldown applied before re-dialing a worker whose connection just closed.
131+ reconnect_cooldown : Duration ,
132+ /// Workers awaiting a cooldown before reconnection.
133+ reconnect_queue : DelayQueue < PeerId > ,
134+ /// Peers currently present in `reconnect_queue`, to avoid scheduling duplicates.
135+ reconnect_scheduled : HashSet < PeerId > ,
124136}
125137
126138#[ allow( dead_code) ]
@@ -195,6 +207,9 @@ impl BaseBehaviour {
195207 maintain_worker_connections : false ,
196208 pending_lookups : Default :: default ( ) ,
197209 max_concurrent_lookups : config. max_concurrent_lookups ,
210+ reconnect_cooldown : config. reconnect_cooldown ,
211+ reconnect_queue : DelayQueue :: new ( ) ,
212+ reconnect_scheduled : HashSet :: new ( ) ,
198213 }
199214 }
200215
@@ -300,9 +315,17 @@ impl BehaviourWrapper for BaseBehaviour {
300315 }
301316 }
302317
303- fn poll ( & mut self , _cx : & mut Context < ' _ > ) -> Poll < impl IntoIterator < Item = TToSwarm < Self > > > {
304- if let Some ( ev) = self . pending_events . pop_front ( ) {
305- return Poll :: Ready ( Some ( ev) ) ;
318+ fn poll ( & mut self , cx : & mut Context < ' _ > ) -> Poll < impl IntoIterator < Item = TToSwarm < Self > > > {
319+ // Drain expired reconnects first, unconditionally: `poll_expired` is what registers
320+ // the timer waker, so it must run even when there's a pending event to emit. Skipping
321+ // it behind the early return below would let a cooldown entry expire without a
322+ // registered waker, deferring the redial until some unrelated event wakes the task.
323+ while let Poll :: Ready ( Some ( expired) ) = self . reconnect_queue . poll_expired ( cx) {
324+ let peer_id = expired. into_inner ( ) ;
325+ self . reconnect_scheduled . remove ( & peer_id) ;
326+ if !self . outbound_conn_exists ( & peer_id) && self . registered_workers . contains ( & peer_id) {
327+ self . pending_lookups . push_back ( peer_id) ;
328+ }
306329 }
307330
308331 while self . ongoing_lookups . len ( ) < self . max_concurrent_lookups {
@@ -316,6 +339,10 @@ impl BehaviourWrapper for BaseBehaviour {
316339 }
317340 }
318341
342+ if let Some ( ev) = self . pending_events . pop_front ( ) {
343+ return Poll :: Ready ( Some ( ev) ) ;
344+ }
345+
319346 Poll :: Pending
320347 }
321348}
@@ -348,9 +375,15 @@ impl BaseBehaviour {
348375 if self . maintain_worker_connections
349376 && !self . outbound_conn_exists ( & peer_id)
350377 && self . registered_workers . contains ( & peer_id)
378+ // Skip if a reconnect is already pending (avoids a tight redial loop and
379+ // unbounded duplicate entries for workers that keep refusing this peer).
380+ && self . reconnect_scheduled . insert ( peer_id)
351381 {
352- log:: debug!( "Worker {peer_id} disconnected, scheduling reconnection" ) ;
353- self . pending_lookups . push_back ( peer_id) ;
382+ log:: debug!(
383+ "Worker {peer_id} disconnected, scheduling reconnection in {:?}" ,
384+ self . reconnect_cooldown
385+ ) ;
386+ self . reconnect_queue . insert ( peer_id, self . reconnect_cooldown ) ;
354387 }
355388 None
356389 }
@@ -526,7 +559,9 @@ impl BaseBehaviour {
526559
527560 if self . maintain_worker_connections {
528561 for peer_id in & self . registered_workers {
529- if !self . outbound_conn_exists ( peer_id) {
562+ if !self . outbound_conn_exists ( peer_id)
563+ && !self . reconnect_scheduled . contains ( peer_id)
564+ {
530565 self . pending_lookups . push_back ( * peer_id) ;
531566 }
532567 }
0 commit comments