@@ -228,15 +228,20 @@ impl<TBvEv> super::Recorder<SwarmEvent<TBvEv>> for Metrics {
228
228
} ,
229
229
cause : cause. as_ref ( ) . map ( Into :: into) ,
230
230
} ;
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
+ }
240
245
}
241
246
SwarmEvent :: IncomingConnection { send_back_addr, .. } => {
242
247
self . connections_incoming
@@ -453,3 +458,90 @@ impl From<&libp2p_swarm::ListenError> for IncomingConnectionError {
453
458
}
454
459
}
455
460
}
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