Skip to content

Commit d2805f5

Browse files
authored
[3/n] bgp: track shutdown completion as distinct from not-requested (#871)
Replace the `AtomicBool` with an atomic over a three-state enum. In upcoming commits in the stack, we're going to test for the value here. Note that `request_shutdown` remains an unconditional store, preserving the previous `store(true)` semantics including a latent hazard where a request landing on an already-stopped runner makes its next start stop immediately. This commit has no externally-visible behavior changes.
1 parent 020c450 commit d2805f5

1 file changed

Lines changed: 145 additions & 27 deletions

File tree

bgp/src/session.rs

Lines changed: 145 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ use mg_api_types::rdb::path::BgpPathProperties;
3939
use mg_api_types::rdb::rib::AddressFamily;
4040
use mg_api_types_versions::{v1, v4};
4141
use mg_common::{IpNetExt, lock, read_lock, write_lock};
42+
use num_enum::{IntoPrimitive, TryFromPrimitive};
4243
use oxnet::{IPV4_NET_WIDTH_MAX, IPV6_NET_WIDTH_MAX, IpNet, Ipv4Net, Ipv6Net};
4344
pub use rdb::DEFAULT_ROUTE_PRIORITY;
4445
use 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)]
15651638
const _: () = {
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

Comments
 (0)