Skip to main content

liminal_server/link/
life.rs

1//! Shared end ownership lets a revocation retire keyed transports synchronously.
2
3use 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/// Exact carrier transitions, independent of crossing observations.
17#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize)]
18pub struct LinkLifeCounters {
19    /// Successfully keyed links.
20    pub links_opened: u64,
21    /// Links whose peer carrier disappeared.
22    pub links_closed_by_peer_gone: u64,
23    /// Links closed by pass revocation.
24    pub links_closed_by_revoked: u64,
25    /// Links ended by a named refusal.
26    pub links_closed_by_refused: u64,
27    /// Links superseded by another link.
28    pub links_closed_by_replaced: u64,
29    /// Links ended by their owner's shutdown.
30    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/// One keyed link's first end, shared by its carrier and observations.
75#[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/// Weak registrations do not retain a departed connection's keyed transports.
210#[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}