1use super::LinkEnd;
4use crate::console::{
5 TopologyChange,
6 records::{Carrier, Link, LinkCounters, LinkState},
7};
8use crate::link_handshake::MachineIdentity;
9use std::collections::BTreeMap;
10use std::sync::atomic::{AtomicU64, Ordering};
11use std::sync::{Arc, Mutex, Weak};
12
13#[cfg(test)]
14mod tests;
15
16#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize)]
18pub struct LinkLifeCounters {
19 pub links_opened: u64,
21 pub links_closed_by_peer_gone: u64,
23 pub links_closed_by_revoked: u64,
25 pub links_closed_by_refused: u64,
27 pub links_closed_by_replaced: u64,
29 pub links_closed_by_shutdown: u64,
31}
32
33#[derive(Debug, Default)]
34struct Totals {
35 opened: AtomicU64,
36 peer_gone: AtomicU64,
37 revoked: AtomicU64,
38 refused: AtomicU64,
39 replaced: AtomicU64,
40 shutdown: AtomicU64,
41}
42
43impl Totals {
44 fn increment(counter: &AtomicU64) -> Result<(), String> {
45 counter
46 .fetch_update(Ordering::AcqRel, Ordering::Acquire, |count| {
47 count.checked_add(1)
48 })
49 .map(|_| ())
50 .map_err(|_| "link lifecycle counter overflow".to_owned())
51 }
52
53 fn snapshot(&self) -> LinkLifeCounters {
54 LinkLifeCounters {
55 links_opened: self.opened.load(Ordering::Acquire),
56 links_closed_by_peer_gone: self.peer_gone.load(Ordering::Acquire),
57 links_closed_by_revoked: self.revoked.load(Ordering::Acquire),
58 links_closed_by_refused: self.refused.load(Ordering::Acquire),
59 links_closed_by_replaced: self.replaced.load(Ordering::Acquire),
60 links_closed_by_shutdown: self.shutdown.load(Ordering::Acquire),
61 }
62 }
63}
64
65type Observer = Arc<dyn Fn(TopologyChange) + Send + Sync>;
66
67#[derive(Default)]
68struct State {
69 end: Option<LinkEnd>,
70 observer: Option<(String, Observer)>,
71 notifier: Option<Arc<dyn Fn() + Send + Sync>>,
72}
73
74#[derive(Clone)]
76pub struct LinkLife {
77 peer: String,
78 totals: Arc<Totals>,
79 state: Arc<Mutex<State>>,
80}
81
82impl std::fmt::Debug for LinkLife {
83 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84 formatter
85 .debug_struct("LinkLife")
86 .field("peer", &self.peer)
87 .finish_non_exhaustive()
88 }
89}
90
91impl LinkLife {
92 pub(crate) fn activity(&self) -> Result<(), String> {
93 let state = self
94 .state
95 .lock()
96 .map_err(|_| "link lifecycle lock poisoned")?;
97 if let Some(notifier) = &state.notifier {
98 notifier();
99 }
100 drop(state);
101 Ok(())
102 }
103 pub(crate) fn notify_with(&self, notifier: Arc<dyn Fn() + Send + Sync>) -> Result<(), String> {
104 let mut state = self
105 .state
106 .lock()
107 .map_err(|_| "link lifecycle lock poisoned")?;
108 state.notifier = Some(notifier);
109 if state.end.is_some()
110 && let Some(notifier) = &state.notifier
111 {
112 notifier();
113 }
114 drop(state);
115 Ok(())
116 }
117 pub(crate) fn peer(&self) -> &str {
118 &self.peer
119 }
120 pub(crate) fn new(peer: MachineIdentity) -> Self {
121 let totals = Totals {
122 opened: AtomicU64::new(1),
123 ..Totals::default()
124 };
125 Self::with_totals(hex::encode(peer.ed25519_public()), Arc::new(totals))
126 }
127
128 fn with_totals(peer: String, totals: Arc<Totals>) -> Self {
129 Self {
130 peer,
131 totals,
132 state: Arc::default(),
133 }
134 }
135
136 pub(crate) fn observe(&self, local: String, observer: Observer) -> Result<(), String> {
137 let mut state = self
138 .state
139 .lock()
140 .map_err(|_| "link lifecycle lock poisoned")?;
141 state.observer = Some((local, observer));
142 self.emit(&state);
143 drop(state);
144 Ok(())
145 }
146
147 pub(crate) fn end(&self, reason: LinkEnd) -> Result<(), String> {
148 let mut state = self
149 .state
150 .lock()
151 .map_err(|_| "link lifecycle lock poisoned")?;
152 if state.end.is_some() {
153 return Ok(());
154 }
155 let counter = match &reason {
156 LinkEnd::PeerGone => &self.totals.peer_gone,
157 LinkEnd::Revoked => &self.totals.revoked,
158 LinkEnd::Refused(_) => &self.totals.refused,
159 LinkEnd::Replaced => &self.totals.replaced,
160 LinkEnd::Shutdown => &self.totals.shutdown,
161 };
162 Totals::increment(counter)?;
163 state.end = Some(reason);
164 self.emit(&state);
165 if let Some(notifier) = &state.notifier {
166 notifier();
167 }
168 drop(state);
169 Ok(())
170 }
171
172 pub(crate) fn snapshot(&self) -> LinkLifeCounters {
173 self.totals.snapshot()
174 }
175
176 pub(crate) fn reason(&self) -> Result<Option<String>, String> {
177 let state = self
178 .state
179 .lock()
180 .map_err(|_| "link lifecycle lock poisoned")?;
181 Ok(state.end.as_ref().map(ToString::to_string))
182 }
183
184 fn emit(&self, state: &State) {
185 if let Some((local, observer)) = &state.observer {
186 observer(TopologyChange::Link {
187 link_id: Link::to_peer(local, &self.peer).id,
188 peer: self.peer.clone(),
189 carrier: Some(Carrier::Wire),
190 state: if state.end.is_some() {
191 LinkState::Down
192 } else {
193 LinkState::Up
194 },
195 reason: state.end.as_ref().map(ToString::to_string),
196 counters: LinkCounters {
197 lifecycle: Some(self.snapshot()),
198 ..LinkCounters::default()
199 },
200 });
201 }
202 }
203}
204
205pub trait CloseLinks: Send + Sync + std::fmt::Debug {
206 fn end_links(&self, reason: LinkEnd) -> Result<(), String>;
207}
208
209#[derive(Clone, Debug, Default)]
211pub struct LinkRegistry {
212 connections: Arc<Mutex<BTreeMap<u64, Weak<dyn CloseLinks>>>>,
213 totals: Arc<Mutex<BTreeMap<String, Arc<Totals>>>>,
214}
215
216impl LinkRegistry {
217 pub(crate) fn register(&self, pid: u64, links: &Arc<dyn CloseLinks>) -> Result<(), String> {
218 self.connections
219 .lock()
220 .map_err(|_| "link registry lock poisoned")?
221 .insert(pid, Arc::downgrade(links));
222 Ok(())
223 }
224
225 pub(crate) fn opened(&self, peer: MachineIdentity) -> Result<LinkLife, String> {
226 let peer = hex::encode(peer.ed25519_public());
227 let mut by_peer = self
228 .totals
229 .lock()
230 .map_err(|_| "link totals lock poisoned")?;
231 let totals = Arc::clone(by_peer.entry(peer.clone()).or_default());
232 drop(by_peer);
233 Totals::increment(&totals.opened)?;
234 Ok(LinkLife::with_totals(peer, totals))
235 }
236
237 pub(crate) fn close(&self, pid: u64, reason: LinkEnd) {
238 let links = self
239 .connections
240 .lock()
241 .map_err(|_| "link registry lock poisoned".to_owned())
242 .map(|connections| connections.get(&pid).and_then(Weak::upgrade));
243 let result = links.and_then(|links| links.map_or(Ok(()), |links| links.end_links(reason)));
244 if let Err(error) = result {
245 tracing::error!(pid, %error, "link closure failed");
246 }
247 }
248
249 pub(crate) fn forget(&self, pid: u64) {
250 match self.connections.lock() {
251 Ok(mut connections) => {
252 connections.remove(&pid);
253 }
254 Err(error) => tracing::error!(pid, %error, "link registration removal failed"),
255 }
256 }
257
258 pub(crate) fn remove(&self, pid: u64) {
259 self.close(pid, LinkEnd::PeerGone);
260 self.forget(pid);
261 }
262}