@@ -228,15 +228,20 @@ impl<TBvEv> super::Recorder<SwarmEvent<TBvEv>> for Metrics {
228228 } ,
229229 cause : cause. as_ref ( ) . map ( Into :: into) ,
230230 } ;
231- self . connections_duration . get_or_create ( & labels) . observe (
232- self . connections
233- . lock ( )
234- . expect ( "lock not to be poisoned" )
235- . remove ( connection_id)
236- . expect ( "closed connection to previously be established" )
237- . elapsed ( )
238- . as_secs_f64 ( ) ,
239- ) ;
231+
232+ // Only record connection duration if we have a record of when it was established.
233+ // This gracefully handles cases where ConnectionClosed events are received
234+ // for connections that were established before metrics collection started.
235+ if let Some ( established_time) = self
236+ . connections
237+ . lock ( )
238+ . expect ( "lock not to be poisoned" )
239+ . remove ( connection_id)
240+ {
241+ self . connections_duration
242+ . get_or_create ( & labels)
243+ . observe ( established_time. elapsed ( ) . as_secs_f64 ( ) ) ;
244+ }
240245 }
241246 SwarmEvent :: IncomingConnection { send_back_addr, .. } => {
242247 self . connections_incoming
@@ -453,3 +458,90 @@ impl From<&libp2p_swarm::ListenError> for IncomingConnectionError {
453458 }
454459 }
455460}
461+
462+ #[ cfg( test) ]
463+ mod tests {
464+ use std:: time:: Duration ;
465+
466+ use libp2p_core:: ConnectedPoint ;
467+ use libp2p_swarm:: { ConnectionId , SwarmEvent } ;
468+ use prometheus_client:: registry:: Registry ;
469+
470+ use super :: * ;
471+ use crate :: Recorder ;
472+
473+ #[ test]
474+ fn test_connection_closed_without_established ( ) {
475+ let mut registry = Registry :: default ( ) ;
476+ let metrics = Metrics :: new ( & mut registry) ;
477+
478+ // Create a fake ConnectionClosed event for a connection that was never tracked.
479+ let connection_id = ConnectionId :: new_unchecked ( 1 ) ;
480+ let endpoint = ConnectedPoint :: Dialer {
481+ address : "/ip4/127.0.0.1/tcp/8080" . parse ( ) . unwrap ( ) ,
482+ role_override : libp2p_core:: Endpoint :: Dialer ,
483+ port_use : libp2p_core:: transport:: PortUse :: New ,
484+ } ;
485+
486+ let event = SwarmEvent :: < ( ) > :: ConnectionClosed {
487+ peer_id : libp2p_identity:: PeerId :: random ( ) ,
488+ connection_id,
489+ endpoint,
490+ num_established : 0 ,
491+ cause : None ,
492+ } ;
493+
494+ // This should NOT panic.
495+ metrics. record ( & event) ;
496+
497+ // Verify that the connections map is still empty (no connection was removed).
498+ let connections = metrics. connections . lock ( ) . expect ( "lock not to be poisoned" ) ;
499+ assert ! ( connections. is_empty( ) ) ;
500+ }
501+
502+ #[ test]
503+ fn test_connection_established_then_closed ( ) {
504+ let mut registry = Registry :: default ( ) ;
505+ let metrics = Metrics :: new ( & mut registry) ;
506+
507+ let connection_id = ConnectionId :: new_unchecked ( 1 ) ;
508+ let endpoint = ConnectedPoint :: Dialer {
509+ address : "/ip4/127.0.0.1/tcp/8080" . parse ( ) . unwrap ( ) ,
510+ role_override : libp2p_core:: Endpoint :: Dialer ,
511+ port_use : libp2p_core:: transport:: PortUse :: New ,
512+ } ;
513+
514+ // First, establish a connection.
515+ let established_event = SwarmEvent :: < ( ) > :: ConnectionEstablished {
516+ peer_id : libp2p_identity:: PeerId :: random ( ) ,
517+ connection_id,
518+ endpoint : endpoint. clone ( ) ,
519+ num_established : std:: num:: NonZeroU32 :: new ( 1 ) . unwrap ( ) ,
520+ concurrent_dial_errors : None ,
521+ established_in : Duration :: from_millis ( 100 ) ,
522+ } ;
523+
524+ metrics. record ( & established_event) ;
525+
526+ // Verify connection was added.
527+ {
528+ let connections = metrics. connections . lock ( ) . expect ( "lock not to be poisoned" ) ;
529+ assert ! ( connections. contains_key( & connection_id) ) ;
530+ }
531+
532+ // Now close the connection.
533+ let closed_event = SwarmEvent :: < ( ) > :: ConnectionClosed {
534+ peer_id : libp2p_identity:: PeerId :: random ( ) ,
535+ connection_id,
536+ endpoint,
537+ num_established : 0 ,
538+ cause : None ,
539+ } ;
540+
541+ metrics. record ( & closed_event) ;
542+
543+ // Verify connection was removed.
544+ let connections = metrics. connections . lock ( ) . expect ( "lock not to be poisoned" ) ;
545+ assert ! ( !connections. contains_key( & connection_id) ) ;
546+ }
547+ }
0 commit comments