Skip to content

Commit 561931c

Browse files
authored
Add state duration to DDM FSM (#689)
* ddm: cleanup peer tracking and add state durations Db.peers was only used as a cache for reporting peers to ddm-api clients calling GET /peers, and the contents of the cache were a bit misleading. For example, there is no such thing as a peer state in DDM. DDM FSMs have states, but they are instantiated per interface not per peer. Not only does the state reported by GET /peers not actually map to a peer, but it doesn't even directly map to the FSM of the peer's interface. Instead, that state was populated by manual db updates scattered throughout the FSM implementation. This removes Db.peers and introduces a new shared InterfaceState struct that consolidates peer/FSM info for a given interface. GET /peers now reads from each InterfaceState and passes along any peers it finds along with interface state. Also fixes a pre-existing bug in oxstats where interface names were always empty due to reading from a stale SmContext clone. * ddmadm: sort output of get-peers * ddm: remove dupe peer_address, add fsm helpers After adding PeerIdentity, the peer_address field of SessionStats is now redundant (and also isn't a stat) so this removes it. The peer address is tracked in PeerIdentity, so we can just rely on that field behind a single mutex instead of locking/juggling both stats and identity. Also adds a couple helper methods for transitioning FSM state and updating the peer state. * ddmadm: show fsm state duration in separate column * cargo fmt Signed-off-by: Trey Aspelund <trey@oxidecomputer.com> * ddmadm: reuse mg_common::format_duration_human Drop the local seconds-precision format_duration helper in favor of the shared helper already used by mgadm for BGP FSM durations, so ddmadm and mgadm display durations consistently. Signed-off-by: Trey Aspelund <trey@oxidecomputer.com> * ddm: add InterfaceState::set_if_info helper Encapsulate the paired writes to if_index and if_name behind a single method on InterfaceState so the two fields stay in sync at the one site that updates them. Signed-off-by: Trey Aspelund <trey@oxidecomputer.com> * ddm: hoist if_name lock out of per-sample oximeter macros The produce() loop locked peer.iface.if_name twelve times per peer per scrape. Take the lock once at the top of the loop body and reuse the cloned String for each sample. Signed-off-by: Trey Aspelund <trey@oxidecomputer.com> * ddm: compute peer_status before locking other iface fields peer_status() internally locks fsm_state and last_fsm_state_change. Call it before acquiring if_index and peer_identity in get_peers so it remains deadlock-safe if it ever takes additional InterfaceState mutexes. Signed-off-by: Trey Aspelund <trey@oxidecomputer.com> --------- Signed-off-by: Trey Aspelund <trey@oxidecomputer.com>
1 parent 2220683 commit 561931c

17 files changed

Lines changed: 333 additions & 74 deletions

File tree

ddm-admin-client/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,8 @@ progenitor::generate_api!(
2323
IpPrefix = ddm_api_types_versions::latest::net::IpPrefix,
2424
Ipv4Prefix = ddm_api_types_versions::latest::net::Ipv4Prefix,
2525
Ipv6Prefix = ddm_api_types_versions::latest::net::Ipv6Prefix,
26+
PeerInfo = ddm_api_types_versions::latest::db::PeerInfo,
27+
PeerStatus = ddm_api_types_versions::latest::db::PeerStatus,
28+
Duration = std::time::Duration,
2629
}
2730
);

ddm-api-types/versions/src/latest.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@ pub mod admin {
1111
}
1212

1313
pub mod db {
14-
pub use crate::v1::db::PeerInfo;
15-
pub use crate::v1::db::PeerStatus;
1614
pub use crate::v1::db::RouterKind;
1715
pub use crate::v1::db::TunnelRoute;
16+
17+
pub use crate::v2::db::PeerInfo;
18+
pub use crate::v2::db::PeerStatus;
1819
}
1920

2021
pub mod exchange {

ddm-api-types/versions/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,5 @@
3232
pub mod latest;
3333
#[path = "initial/mod.rs"]
3434
pub mod v1;
35+
#[path = "peer_durations/mod.rs"]
36+
pub mod v2;
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4+
5+
use std::net::Ipv6Addr;
6+
use std::time::Duration;
7+
8+
use crate::v1::db::RouterKind;
9+
use schemars::JsonSchema;
10+
use serde::{Deserialize, Serialize};
11+
12+
/// Status of a DDM peer, including how long the peer has been in its
13+
/// current state.
14+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
15+
#[serde(tag = "type", content = "duration")]
16+
pub enum PeerStatus {
17+
Init(Duration),
18+
Solicit(Duration),
19+
Exchange(Duration),
20+
Expired(Duration),
21+
}
22+
23+
/// Information about a DDM peer.
24+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
25+
pub struct PeerInfo {
26+
pub status: PeerStatus,
27+
pub addr: Ipv6Addr,
28+
pub host: String,
29+
pub kind: RouterKind,
30+
}
31+
32+
// Response backwards-compat: convert v2 PeerInfo to v1 PeerInfo.
33+
impl From<PeerInfo> for crate::v1::db::PeerInfo {
34+
fn from(value: PeerInfo) -> Self {
35+
Self {
36+
status: match value.status {
37+
PeerStatus::Init(_) | PeerStatus::Solicit(_) => {
38+
crate::v1::db::PeerStatus::NoContact
39+
}
40+
PeerStatus::Exchange(_) => crate::v1::db::PeerStatus::Active,
41+
PeerStatus::Expired(_) => crate::v1::db::PeerStatus::Expired,
42+
},
43+
addr: value.addr,
44+
host: value.host,
45+
kind: value.kind,
46+
}
47+
}
48+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4+
5+
//! Version `PEER_DURATIONS` of the DDM Admin API.
6+
//!
7+
//! Tracks how long each DDM peer has been in its current state and exposes
8+
//! that through the `/peers` endpoint with duration information.
9+
10+
pub mod db;

ddm-api/src/lib.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
44

55
use ddm_api_types_versions::latest;
6+
use ddm_api_types_versions::v1;
67
use dropshot::HttpError;
78
use dropshot::HttpResponseOk;
89
use dropshot::HttpResponseUpdatedNoContent;
@@ -25,6 +26,7 @@ api_versions!([
2526
// | example for the next person.
2627
// v
2728
// (next_int, IDENT),
29+
(2, PEER_DURATIONS),
2830
(1, INITIAL),
2931
]);
3032

@@ -44,11 +46,21 @@ api_versions!([
4446
pub trait DdmAdminApi {
4547
type Context;
4648

47-
#[endpoint { method = GET, path = "/peers" }]
49+
#[endpoint { method = GET, path = "/peers", versions = VERSION_PEER_DURATIONS.. }]
4850
async fn get_peers(
4951
ctx: RequestContext<Self::Context>,
5052
) -> Result<HttpResponseOk<HashMap<u32, latest::db::PeerInfo>>, HttpError>;
5153

54+
#[endpoint { method = GET, path = "/peers", versions = ..VERSION_PEER_DURATIONS }]
55+
async fn get_peers_v1(
56+
ctx: RequestContext<Self::Context>,
57+
) -> Result<HttpResponseOk<HashMap<u32, v1::db::PeerInfo>>, HttpError> {
58+
let resp = Self::get_peers(ctx).await?;
59+
let converted: HashMap<u32, v1::db::PeerInfo> =
60+
resp.0.into_iter().map(|(k, v)| (k, v.into())).collect();
61+
Ok(HttpResponseOk(converted))
62+
}
63+
5264
#[endpoint { method = DELETE, path = "/peers/{addr}" }]
5365
async fn expire_peer(
5466
ctx: RequestContext<Self::Context>,

ddm/src/admin.rs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,26 @@ impl DdmAdminApi for DdmAdminApiImpl {
122122
ctx: RequestContext<Self::Context>,
123123
) -> Result<HttpResponseOk<HashMap<u32, PeerInfo>>, HttpError> {
124124
let ctx = lock!(ctx.context());
125-
Ok(HttpResponseOk(ctx.db.peers()))
125+
let mut result = HashMap::new();
126+
for sm in &ctx.peers {
127+
// Compute status first so peer_status() never runs while we hold
128+
// any of the InterfaceState mutexes below.
129+
let status = sm.iface.peer_status();
130+
let if_index = *lock!(sm.iface.if_index);
131+
let Some(peer) = lock!(sm.iface.peer_identity).clone() else {
132+
continue;
133+
};
134+
result.insert(
135+
if_index,
136+
PeerInfo {
137+
status,
138+
addr: peer.addr,
139+
host: peer.hostname,
140+
kind: peer.kind,
141+
},
142+
);
143+
}
144+
Ok(HttpResponseOk(result))
126145
}
127146

128147
async fn expire_peer(

ddm/src/db.rs

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// License, v. 2.0. If a copy of the MPL was not distributed with this
33
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
44

5-
use ddm_api_types::db::{PeerInfo, TunnelRoute};
5+
use ddm_api_types::db::TunnelRoute;
66
use ddm_api_types::net::TunnelOrigin;
77
use mg_common::lock;
88
use oxnet::{IpNet, Ipv6Net};
@@ -45,7 +45,6 @@ pub struct Db {
4545

4646
#[derive(Default, Clone)]
4747
pub struct DbData {
48-
pub peers: HashMap<u32, PeerInfo>,
4948
pub imported: HashSet<Route>,
5049
pub imported_tunnel: HashSet<TunnelRoute>,
5150
}
@@ -65,10 +64,6 @@ impl Db {
6564
lock!(self.data).clone()
6665
}
6766

68-
pub fn peers(&self) -> HashMap<u32, PeerInfo> {
69-
lock!(self.data).peers.clone()
70-
}
71-
7267
pub fn imported(&self) -> HashSet<Route> {
7368
lock!(self.data).imported.clone()
7469
}
@@ -221,15 +216,6 @@ impl Db {
221216
Ok(())
222217
}
223218

224-
/// Set peer info at the given index. Returns true if peer information was
225-
/// changed.
226-
pub fn set_peer(&self, index: u32, info: PeerInfo) -> bool {
227-
match lock!(self.data).peers.insert(index, info.clone()) {
228-
Some(previous) => previous == info,
229-
None => true,
230-
}
231-
}
232-
233219
pub fn remove_nexthop_routes(
234220
&self,
235221
nexthop: Ipv6Addr,
@@ -259,10 +245,6 @@ impl Db {
259245
(removed, tnl_removed)
260246
}
261247

262-
pub fn remove_peer(&self, index: u32) {
263-
lock!(self.data).peers.remove(&index);
264-
}
265-
266248
pub fn routes_by_vector(
267249
&self,
268250
dst: Ipv6Net,

ddm/src/discovery/runtime.rs

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,11 @@
88
//! illumos-only.
99
1010
use super::{DiscoveryError, Version};
11-
use crate::db::Db;
12-
use crate::sm::{Config, Event, NeighborEvent, SessionStats};
11+
use crate::sm::{
12+
Config, Event, InterfaceState, NeighborEvent, PeerIdentity, SessionStats,
13+
};
1314
use crate::{dbg, err, inf, trc, wrn};
14-
use ddm_api_types::db::{PeerInfo, PeerStatus, RouterKind};
15+
use ddm_api_types::db::RouterKind;
1516
use mg_common::lock;
1617
use serde::{Deserialize, Serialize};
1718
use slog::Logger;
@@ -80,7 +81,7 @@ struct HandlerContext {
8081
nbr: Arc<RwLock<Option<Neighbor>>>,
8182
log: Logger,
8283
event: Sender<Event>,
83-
db: Db,
84+
iface: Arc<InterfaceState>,
8485
}
8586

8687
struct Neighbor {
@@ -94,7 +95,7 @@ pub(crate) fn handler(
9495
hostname: String,
9596
config: Config,
9697
event: Sender<Event>,
97-
db: Db,
98+
iface: Arc<InterfaceState>,
9899
stats: Arc<SessionStats>,
99100
log: Logger,
100101
) -> Result<(), DiscoveryError> {
@@ -134,7 +135,7 @@ pub(crate) fn handler(
134135
hostname,
135136
event,
136137
config,
137-
db,
138+
iface,
138139
};
139140

140141
let stop = Arc::new(AtomicBool::new(false));
@@ -426,17 +427,15 @@ fn handle_advertisement(
426427
}
427428
};
428429
drop(guard);
429-
let updated = ctx.db.set_peer(
430-
ctx.config.if_index,
431-
PeerInfo {
432-
status: PeerStatus::Active,
433-
addr: *sender,
434-
host: hostname,
435-
kind,
436-
},
437-
);
438-
if updated {
439-
lock!(stats.peer_address).replace(*sender);
430+
let new_peer = PeerIdentity {
431+
addr: *sender,
432+
hostname,
433+
kind,
434+
};
435+
let mut info = lock!(ctx.iface.peer_identity);
436+
if info.as_ref() != Some(&new_peer) {
437+
*info = Some(new_peer);
438+
drop(info);
440439
emit_nbr_update(ctx, sender, version);
441440
}
442441
}

0 commit comments

Comments
 (0)