@@ -39,6 +39,7 @@ use mg_api_types::rdb::path::BgpPathProperties;
3939use mg_api_types:: rdb:: rib:: AddressFamily ;
4040use mg_api_types_versions:: { v1, v4} ;
4141use mg_common:: { IpNetExt , lock, read_lock, write_lock} ;
42+ use num_enum:: { IntoPrimitive , TryFromPrimitive } ;
4243use oxnet:: { IPV4_NET_WIDTH_MAX , IPV6_NET_WIDTH_MAX , IpNet , Ipv4Net , Ipv6Net } ;
4344pub use rdb:: DEFAULT_ROUTE_PRIORITY ;
4445use rdb:: { Asn , Db } ;
@@ -52,7 +53,7 @@ use std::{
5253 num:: NonZeroU16 ,
5354 sync:: {
5455 Arc , Mutex , RwLock ,
55- atomic:: { AtomicBool , AtomicU64 , Ordering } ,
56+ atomic:: { AtomicU8 , AtomicU64 , Ordering } ,
5657 mpsc:: { Receiver , Sender } ,
5758 } ,
5859 time:: { Duration , Instant } ,
@@ -1561,6 +1562,78 @@ impl<Cnx: BgpConnection> Default for ConnectionRegistry<Cnx> {
15611562 }
15621563}
15631564
1565+ /// Where a session runner is in the shutdown lifecycle.
1566+ ///
1567+ /// This is distinct from `FsmStateKind`, which tracks the BGP protocol
1568+ /// state machine.
1569+ #[ derive(
1570+ Clone , Copy , Debug , Eq , PartialEq , IntoPrimitive , TryFromPrimitive ,
1571+ ) ]
1572+ #[ repr( u8 ) ]
1573+ pub ( crate ) enum ShutdownState {
1574+ /// No shutdown has ever been requested.
1575+ ///
1576+ /// This is the initial state; a runner can never return to it.
1577+ NotRequested = 0 ,
1578+
1579+ /// A shutdown has been requested, but not yet completed.
1580+ ///
1581+ /// The state transitions here from any state on
1582+ /// `SessionRunner::shutdown()`.
1583+ Requested = 1 ,
1584+
1585+ /// A requested shutdown finished.
1586+ ///
1587+ /// Set when `on_shutdown` completes. This could be combined with
1588+ /// `NotRequested` in principle, but keeping them distinct allows a
1589+ /// completed shutdown to be observable in tests.
1590+ Complete = 2 ,
1591+ }
1592+
1593+ struct AtomicShutdownState ( AtomicU8 ) ;
1594+
1595+ impl AtomicShutdownState {
1596+ fn new ( ) -> Self {
1597+ Self ( AtomicU8 :: new ( ShutdownState :: NotRequested . into ( ) ) )
1598+ }
1599+
1600+ fn load ( & self ) -> ShutdownState {
1601+ ShutdownState :: try_from ( self . 0 . load ( Ordering :: Acquire ) )
1602+ . expect ( "atomic holds a ShutdownState discriminant" )
1603+ }
1604+
1605+ fn is_requested ( & self ) -> bool {
1606+ match self . load ( ) {
1607+ ShutdownState :: Requested => true ,
1608+ ShutdownState :: NotRequested | ShutdownState :: Complete => false ,
1609+ }
1610+ }
1611+
1612+ /// Move to `Requested`.
1613+ ///
1614+ /// `Complete -> Requested` is a valid transition if the session is running.
1615+ /// But there's a hazard here: a request landing on an already-stopped
1616+ /// runner will make its next start immediately stop.
1617+ fn request_shutdown ( & self ) {
1618+ self . 0
1619+ . store ( ShutdownState :: Requested . into ( ) , Ordering :: Release ) ;
1620+ }
1621+
1622+ /// Transition from `Requested` to `Complete`.
1623+ fn finish_shutdown ( & self ) -> Result < ( ) , ShutdownState > {
1624+ match self . 0 . compare_exchange (
1625+ ShutdownState :: Requested . into ( ) ,
1626+ ShutdownState :: Complete . into ( ) ,
1627+ Ordering :: AcqRel ,
1628+ Ordering :: Acquire ,
1629+ ) {
1630+ Ok ( _) => Ok ( ( ) ) ,
1631+ Err ( observed) => Err ( ShutdownState :: try_from ( observed)
1632+ . expect ( "atomic holds a ShutdownState discriminant" ) ) ,
1633+ }
1634+ }
1635+ }
1636+
15641637#[ expect( dead_code) ]
15651638const _: ( ) = {
15661639 const fn assert_send_sync < T : Send + Sync > ( ) { }
@@ -1662,7 +1735,7 @@ pub struct SessionRunner<Cnx: BgpConnection + 'static> {
16621735 /// Capabilities to send to the peer
16631736 pub caps_tx : Arc < Mutex < BTreeSet < Capability > > > ,
16641737
1665- shutdown : AtomicBool ,
1738+ shutdown_state : AtomicShutdownState ,
16661739 db : Db ,
16671740 fanout4 : Arc < RwLock < Fanout4 < Cnx > > > ,
16681741 fanout6 : Arc < RwLock < Fanout6 < Cnx > > > ,
@@ -1779,7 +1852,7 @@ impl<Cnx: BgpConnection + 'static> SessionRunner<Cnx> {
17791852 router. log . clone ( ) ,
17801853 ) ) ,
17811854 log : router. log . clone ( ) ,
1782- shutdown : AtomicBool :: new ( false ) ,
1855+ shutdown_state : AtomicShutdownState :: new ( ) ,
17831856 fanout4 : router. fanout4 . clone ( ) ,
17841857 fanout6 : router. fanout6 . clone ( ) ,
17851858 router : router. clone ( ) ,
@@ -1865,16 +1938,16 @@ impl<Cnx: BgpConnection + 'static> SessionRunner<Cnx> {
18651938 }
18661939
18671940 /// Request a peer session shutdown. Does not shut down the session right
1868- /// away. Simply sets a flag that the session is to be shut down which will
1869- /// be acted upon in the state machine loop .
1941+ /// away. Records the request, which the state machine loop will observe and
1942+ /// act upon at its next check .
18701943 pub fn shutdown ( & self ) {
18711944 session_log_lite ! (
18721945 self ,
18731946 info,
1874- "session runner (peer {}) received shutdown request, setting shutdown flag " ,
1947+ "session runner (peer {}) received shutdown request" ,
18751948 self . peer_id( ) ;
18761949 ) ;
1877- self . shutdown . store ( true , Ordering :: Release ) ;
1950+ self . shutdown_state . request_shutdown ( ) ;
18781951 }
18791952
18801953 /// Join a connector thread and handle any panic, logging appropriately
@@ -2236,11 +2309,11 @@ impl<Cnx: BgpConnection + 'static> SessionRunner<Cnx> {
22362309
22372310 loop {
22382311 // Check to see if a shutdown has been requested.
2239- if self . shutdown . load ( Ordering :: Acquire ) {
2312+ if self . shutdown_state . is_requested ( ) {
22402313 session_log_lite ! (
22412314 self ,
22422315 info,
2243- "session runner (peer: {}) caught shutdown flag " ,
2316+ "session runner (peer: {}) observed shutdown request " ,
22442317 self . peer_id( ) ;
22452318 ) ;
22462319 return ;
@@ -2396,7 +2469,7 @@ impl<Cnx: BgpConnection + 'static> SessionRunner<Cnx> {
23962469
23972470 loop {
23982471 // Check to see if a shutdown has been requested.
2399- if self . shutdown . load ( Ordering :: Acquire ) {
2472+ if self . shutdown_state . is_requested ( ) {
24002473 return FsmState :: Idle ;
24012474 }
24022475
@@ -2631,7 +2704,7 @@ impl<Cnx: BgpConnection + 'static> SessionRunner<Cnx> {
26312704 fn fsm_connect ( & self , event_rx : & Receiver < FsmEvent < Cnx > > ) -> FsmState < Cnx > {
26322705 loop {
26332706 // Check to see if a shutdown has been requested.
2634- if self . shutdown . load ( Ordering :: Acquire ) {
2707+ if self . shutdown_state . is_requested ( ) {
26352708 return FsmState :: Idle ;
26362709 }
26372710
@@ -2969,7 +3042,7 @@ impl<Cnx: BgpConnection + 'static> SessionRunner<Cnx> {
29693042 fn fsm_active ( & self , event_rx : & Receiver < FsmEvent < Cnx > > ) -> FsmState < Cnx > {
29703043 loop {
29713044 // Check to see if a shutdown has been requested.
2972- if self . shutdown . load ( Ordering :: Acquire ) {
3045+ if self . shutdown_state . is_requested ( ) {
29733046 return FsmState :: Idle ;
29743047 }
29753048
@@ -3346,7 +3419,7 @@ impl<Cnx: BgpConnection + 'static> SessionRunner<Cnx> {
33463419 ) -> FsmState < Cnx > {
33473420 let om = loop {
33483421 // Check to see if a shutdown has been requested.
3349- if self . shutdown . load ( Ordering :: Acquire ) {
3422+ if self . shutdown_state . is_requested ( ) {
33503423 return FsmState :: Idle ;
33513424 }
33523425
@@ -3925,7 +3998,7 @@ impl<Cnx: BgpConnection + 'static> SessionRunner<Cnx> {
39253998 pc : PeerConnection < Cnx > ,
39263999 ) -> FsmState < Cnx > {
39274000 // Check to see if a shutdown has been requested.
3928- if self . shutdown . load ( Ordering :: Acquire ) {
4001+ if self . shutdown_state . is_requested ( ) {
39294002 return FsmState :: Idle ;
39304003 }
39314004
@@ -4460,7 +4533,7 @@ impl<Cnx: BgpConnection + 'static> SessionRunner<Cnx> {
44604533 ) -> FsmState < Cnx > {
44614534 let om = loop {
44624535 // Check to see if a shutdown has been requested.
4463- if self . shutdown . load ( Ordering :: Acquire ) {
4536+ if self . shutdown_state . is_requested ( ) {
44644537 return FsmState :: Idle ;
44654538 }
44664539
@@ -5315,7 +5388,7 @@ impl<Cnx: BgpConnection + 'static> SessionRunner<Cnx> {
53155388 ) -> FsmState < Cnx > {
53165389 loop {
53175390 // Check to see if a shutdown has been requested.
5318- if self . shutdown . load ( Ordering :: Acquire ) {
5391+ if self . shutdown_state . is_requested ( ) {
53195392 return FsmState :: Idle ;
53205393 }
53215394
@@ -6089,7 +6162,7 @@ impl<Cnx: BgpConnection + 'static> SessionRunner<Cnx> {
60896162 /// Sync up with peers.
60906163 fn fsm_session_setup ( & self , pc : PeerConnection < Cnx > ) -> FsmState < Cnx > {
60916164 // Check to see if a shutdown has been requested.
6092- if self . shutdown . load ( Ordering :: Acquire ) {
6165+ if self . shutdown_state . is_requested ( ) {
60936166 return FsmState :: Idle ;
60946167 }
60956168
@@ -6244,7 +6317,7 @@ impl<Cnx: BgpConnection + 'static> SessionRunner<Cnx> {
62446317 pc : PeerConnection < Cnx > ,
62456318 ) -> FsmState < Cnx > {
62466319 // Check to see if a shutdown has been requested.
6247- if self . shutdown . load ( Ordering :: Acquire ) {
6320+ if self . shutdown_state . is_requested ( ) {
62486321 return self . exit_established ( pc) ;
62496322 }
62506323
@@ -7200,15 +7273,27 @@ impl<Cnx: BgpConnection + 'static> SessionRunner<Cnx> {
72007273 * ( lock ! ( self . state) ) = next;
72017274 }
72027275
7203- // Reset the shutdown signal.
7204- self . shutdown . store ( false , Ordering :: Release ) ;
7205-
7206- session_log_lite ! (
7207- self ,
7208- info,
7209- "session runner (peer {}): shutdown complete" ,
7210- self . peer_id( )
7211- ) ;
7276+ match self . shutdown_state . finish_shutdown ( ) {
7277+ Ok ( ( ) ) => {
7278+ session_log_lite ! (
7279+ self ,
7280+ info,
7281+ "session runner (peer {}): shutdown complete" ,
7282+ self . peer_id( )
7283+ ) ;
7284+ }
7285+ Err ( observed) => {
7286+ // This is unreachable as of this writing. Error loudly, so it's
7287+ // flagged in logs in case this ever changes.
7288+ session_log_lite ! (
7289+ self ,
7290+ error,
7291+ "session runner (peer {}): shutdown housekeeping ran, \
7292+ but state was {observed:?}, not Requested",
7293+ self . peer_id( )
7294+ ) ;
7295+ }
7296+ }
72127297 }
72137298
72147299 /// Send an event to the state machine driving this peer session.
@@ -9275,6 +9360,39 @@ mod tests {
92759360 use mg_common:: * ;
92769361 use std:: net:: { Ipv4Addr , Ipv6Addr } ;
92779362
9363+ #[ test]
9364+ fn shutdown_state_transitions ( ) {
9365+ let s = AtomicShutdownState :: new ( ) ;
9366+ assert_eq ! ( s. load( ) , ShutdownState :: NotRequested ) ;
9367+ assert ! ( !s. is_requested( ) ) ;
9368+
9369+ // Invalid state transition: NotRequested -> Complete.
9370+ assert_eq ! ( s. finish_shutdown( ) , Err ( ShutdownState :: NotRequested ) ) ;
9371+ assert_eq ! ( s. load( ) , ShutdownState :: NotRequested ) ;
9372+
9373+ // NotRequested -> Requested.
9374+ s. request_shutdown ( ) ;
9375+ assert_eq ! ( s. load( ) , ShutdownState :: Requested ) ;
9376+ assert ! ( s. is_requested( ) ) ;
9377+
9378+ // Requested -> Requested (a no-op).
9379+ s. request_shutdown ( ) ;
9380+ assert_eq ! ( s. load( ) , ShutdownState :: Requested ) ;
9381+
9382+ // Requested -> Complete.
9383+ assert_eq ! ( s. finish_shutdown( ) , Ok ( ( ) ) ) ;
9384+ assert_eq ! ( s. load( ) , ShutdownState :: Complete ) ;
9385+ assert ! ( !s. is_requested( ) ) ;
9386+
9387+ // Complete -> Complete (a no-op).
9388+ assert_eq ! ( s. finish_shutdown( ) , Err ( ShutdownState :: Complete ) ) ;
9389+ assert_eq ! ( s. load( ) , ShutdownState :: Complete ) ;
9390+
9391+ // Complete -> Requested.
9392+ s. request_shutdown ( ) ;
9393+ assert_eq ! ( s. load( ) , ShutdownState :: Requested ) ;
9394+ }
9395+
92789396 #[ test]
92799397 fn test_resolve_collision_decision ( ) {
92809398 use crate :: connection:: ConnectionDirection ;
0 commit comments