Skip to main content

liminal_server/link_bridge/
fault.rs

1//! Fault records remain readable independently of the state that was refused.
2
3use super::{BridgeCounters, ServerError, failure, life, lock};
4use std::collections::BTreeMap;
5use std::net::{Shutdown, TcpStream};
6use std::sync::atomic::AtomicU64;
7#[cfg(feature = "test-support")]
8use std::sync::atomic::Ordering;
9use std::sync::{Arc, Condvar, Mutex, OnceLock, Weak};
10
11#[derive(Debug, Default)]
12pub(super) struct Observation {
13    pub(super) life: OnceLock<crate::link::LinkLife>,
14    previous_lifecycle: crate::link::LinkLifeCounters,
15    #[cfg(feature = "test-support")]
16    registry_signal: OnceLock<RegistrySignal>,
17    #[cfg(feature = "test-support")]
18    pub(super) stop_before_ack: std::sync::atomic::AtomicBool,
19    #[cfg(feature = "test-support")]
20    pub(super) prepared_crossing: Mutex<Option<super::prepared::PreparedCrossing>>,
21    pub(super) counters: Mutex<BridgeCounters>,
22    pub(super) settled: AtomicU64,
23    pub(super) changed: Condvar,
24    pub(super) wake: Arc<Wake>,
25}
26
27impl Observation {
28    #[cfg(test)]
29    pub(super) fn new(interrupt: TcpStream) -> Arc<Self> {
30        Self::with_counts(Some(interrupt), BridgeCounters::default(), 0)
31    }
32
33    pub(super) fn resume(
34        interrupt: TcpStream,
35        counters: BridgeCounters,
36    ) -> Result<Arc<Self>, ServerError> {
37        Self::retaining(Some(interrupt), counters)
38    }
39    pub(super) fn retaining(
40        interrupt: Option<TcpStream>,
41        counters: BridgeCounters,
42    ) -> Result<Arc<Self>, ServerError> {
43        let settled = counters
44            .crossed
45            .checked_add(counters.refused)
46            .and_then(|count| count.checked_add(counters.too_large))
47            .ok_or_else(|| failure("configured link answer counter overflow"))?;
48        Ok(Self::with_counts(interrupt, counters, settled))
49    }
50    fn with_counts(
51        interrupt: Option<TcpStream>,
52        counters: BridgeCounters,
53        settled: u64,
54    ) -> Arc<Self> {
55        Arc::new_cyclic(|observation| Self {
56            wake: Arc::new(Wake {
57                observation: observation.clone(),
58                interrupt,
59                ..Wake::default()
60            }),
61            previous_lifecycle: counters.lifecycle,
62            counters: Mutex::new(counters),
63            settled: AtomicU64::new(settled),
64            ..Self::default()
65        })
66    }
67    pub(super) fn notify_changes(&self) {
68        self.changed.notify_all();
69        #[cfg(feature = "test-support")]
70        if let Some((registry, changed)) = self.registry_signal.get()
71            && let (Some(registry), Some(changed)) = (registry.upgrade(), changed.upgrade())
72        {
73            match lock(&registry, "registry notification") {
74                Ok(guard) => {
75                    changed.notify_all();
76                    drop(guard);
77                }
78                Err(error) => self.wake.refuse(&error),
79            }
80        }
81    }
82
83    pub(super) fn snapshot(&self) -> BridgeCounters {
84        let mut counters = match lock(&self.counters, "counters snapshot") {
85            Ok(counters) => counters.clone(),
86            Err(error) => {
87                self.wake.refuse(&error);
88                BridgeCounters::default()
89            }
90        };
91        counters.error = self.wake.reason.get().cloned().or(counters.error);
92        match super::counts::lifecycle(life::counts(self.life.get()), self.previous_lifecycle) {
93            Ok(lifecycle) => counters.lifecycle = lifecycle,
94            Err(error) => {
95                self.wake.refuse(&error);
96                counters.error = Some(error.to_string());
97            }
98        }
99        counters
100    }
101
102    #[cfg(feature = "test-support")]
103    pub(super) fn observe_answers(&self, count: u64, bound: std::time::Duration) -> bool {
104        let result = lock(&self.counters, "answer observation").and_then(|counters| {
105            self.changed
106                .wait_timeout_while(counters, bound, |counters| {
107                    self.settled.load(Ordering::Acquire) < count && counters.error.is_none()
108                })
109                .map_err(|_| failure("link_bridge_poisoned: answer observation"))
110        });
111        match result {
112            Ok((counters, _)) => {
113                let observed = self.settled.load(Ordering::Acquire) >= count
114                    && counters.error.is_none()
115                    && self.wake.check().is_ok();
116                drop(counters);
117                observed
118            }
119            Err(error) => {
120                self.wake.refuse(&error);
121                false
122            }
123        }
124    }
125}
126
127type RegistryMap = Mutex<BTreeMap<String, Arc<Observation>>>;
128#[cfg(feature = "test-support")]
129type RegistrySignal = (Weak<RegistryMap>, Weak<Condvar>);
130
131/// Live per-link observations shared with the health endpoint.
132#[derive(Clone, Debug, Default)]
133pub struct BridgeRegistry(
134    pub(super) Arc<RegistryMap>,
135    Arc<RegistryFault>,
136    Arc<Condvar>,
137);
138
139#[derive(Debug, Default)]
140struct RegistryFault {
141    reason: OnceLock<String>,
142    stops: OnceLock<Box<RegistryStop>>,
143}
144
145#[derive(Debug)]
146struct RegistryStop {
147    id: String,
148    wake: Mutex<Weak<Wake>>,
149    next: OnceLock<Box<Self>>,
150}
151
152impl BridgeRegistry {
153    /// Bounds only an observation of the registry's signalled changes.
154    #[cfg(feature = "test-support")]
155    /// # Errors
156    /// The registry lock was poisoned.
157    pub fn observe(
158        &self,
159        bound: std::time::Duration,
160        mut reached: impl FnMut(&BTreeMap<String, super::BridgeCounters>) -> bool,
161    ) -> Result<bool, ServerError> {
162        let (observations, _) = self
163            .2
164            .wait_timeout_while(
165                lock(&self.0, "registry observation")?,
166                bound,
167                |observations| {
168                    let snapshot = observations
169                        .iter()
170                        .map(|(id, observation)| (id.clone(), observation.snapshot()))
171                        .collect();
172                    !reached(&snapshot)
173                },
174            )
175            .map_err(|_| failure("registry observation poisoned"))?;
176        let snapshot = observations
177            .iter()
178            .map(|(id, observation)| (id.clone(), observation.snapshot()))
179            .collect();
180        drop(observations);
181        Ok(reached(&snapshot))
182    }
183
184    /// Reads all configured bridge counters without scheduling or touching storage.
185    #[must_use]
186    pub fn snapshot(&self) -> BTreeMap<String, BridgeCounters> {
187        match lock(&self.0, "health registry") {
188            Ok(observations) => observations
189                .iter()
190                .map(|(id, observation)| (id.clone(), observation.snapshot()))
191                .collect(),
192            Err(error) => {
193                self.refuse(&error);
194                BTreeMap::new()
195            }
196        }
197    }
198
199    pub(crate) fn remember(&self, id: String, counters: BridgeCounters) -> Result<(), ServerError> {
200        let observation = Observation::retaining(None, counters)?;
201        self.install(id, &observation);
202        observation.wake.check()
203    }
204    pub(crate) fn dial_refused(&self, id: &str, reason: String) -> Result<(), ServerError> {
205        let mut counters = self.snapshot().remove(id).unwrap_or_default();
206        counters.dial_refused = counters
207            .dial_refused
208            .checked_add(1)
209            .ok_or_else(|| failure("configured dial refusal counter overflow"))?;
210        counters.last_refusal = Some(reason);
211        self.remember(id.to_owned(), counters)
212    }
213    pub(crate) fn open_refused(&self, id: &str, reason: String) -> Result<bool, ServerError> {
214        let mut counters = self.snapshot().remove(id).unwrap_or_default();
215        let changed = counters.last_refusal.as_ref() != Some(&reason);
216        counters.refused = counters
217            .refused
218            .checked_add(1)
219            .ok_or_else(|| failure("configured open refusal counter overflow"))?;
220        counters.last_refusal = Some(reason);
221        self.remember(id.to_owned(), counters)?;
222        Ok(changed)
223    }
224    #[cfg(feature = "test-support")]
225    pub(crate) fn observe_answers(
226        &self,
227        ids: &[String],
228        count: u64,
229        bound: std::time::Duration,
230    ) -> Result<bool, ServerError> {
231        let reached = |observations: &BTreeMap<String, Arc<Observation>>| {
232            ids.iter().all(|id| {
233                observations.get(id).is_some_and(|observation| {
234                    observation.settled.load(Ordering::Acquire) >= count
235                        && observation.snapshot().error.is_none()
236                })
237            })
238        };
239        let (observations, _) = self
240            .2
241            .wait_timeout_while(
242                lock(&self.0, "configured answer observation")?,
243                bound,
244                |observations| !reached(observations),
245            )
246            .map_err(|_| failure("configured answer observation poisoned"))?;
247        let observed = reached(&observations);
248        drop(observations);
249        Ok(observed)
250    }
251    pub(super) fn install(&self, id: String, observation: &Arc<Observation>) {
252        if let Err(error) = self.register_stop(&id, observation) {
253            observation.wake.refuse(&error);
254            return;
255        }
256        #[cfg(feature = "test-support")]
257        if observation
258            .registry_signal
259            .set((Arc::downgrade(&self.0), Arc::downgrade(&self.2)))
260            .is_err()
261        {
262            observation
263                .wake
264                .refuse(&failure("bridge observation registry already installed"));
265            return;
266        }
267        let result = lock(&self.0, "health registry installation").map(|mut registry| {
268            registry.insert(id, Arc::clone(observation));
269            self.2.notify_all();
270        });
271        if let Err(error) = result {
272            self.refuse(&error);
273        }
274        if let Some(reason) = self.1.reason.get() {
275            observation.wake.refuse(&failure(reason.clone()));
276        }
277    }
278    fn register_stop(&self, id: &str, observation: &Arc<Observation>) -> Result<(), ServerError> {
279        // Replacement bridges update the same stop node instead of retaining an outage's history.
280        let mut next = &self.1.stops;
281        loop {
282            if let Some(existing) = next.get() {
283                if existing.id == id {
284                    *lock(&existing.wake, "registry stop replacement")? =
285                        Arc::downgrade(&observation.wake);
286                    return Ok(());
287                }
288                next = &existing.next;
289                continue;
290            }
291            let stop = Box::new(RegistryStop {
292                id: id.to_owned(),
293                wake: Mutex::new(Arc::downgrade(&observation.wake)),
294                next: OnceLock::new(),
295            });
296            if next.set(stop).is_ok() {
297                return Ok(());
298            }
299        }
300    }
301
302    fn refuse(&self, error: &ServerError) {
303        if self.1.reason.set(error.to_string()).is_err() {
304            return;
305        }
306        tracing::error!(%error, "link bridge registry refused poisoned state");
307        let mut next = self.1.stops.get();
308        while let Some(stop) = next {
309            match lock(&stop.wake, "registry stop refusal") {
310                Ok(wake) => {
311                    if let Some(wake) = wake.upgrade() {
312                        wake.refuse(error);
313                    }
314                }
315                Err(error) => tracing::error!(%error, "registry stop refused poisoned state"),
316            }
317            next = stop.next.get();
318        }
319    }
320}
321
322#[derive(Debug, Default)]
323pub(super) struct WakeState {
324    ready: bool,
325    stopping: bool,
326}
327
328#[derive(Debug, Default)]
329pub(super) struct Wake {
330    pub(super) state: Mutex<WakeState>,
331    changed: Condvar,
332    reason: OnceLock<String>,
333    observation: Weak<Observation>,
334    interrupt: Option<TcpStream>,
335}
336
337impl Wake {
338    pub(super) fn notify(&self) {
339        match lock(&self.state, "wake notification") {
340            Ok(mut state) => state.ready = true,
341            Err(error) => self.refuse(&error),
342        }
343        self.changed.notify_one();
344    }
345
346    pub(super) fn stopping(&self) -> bool {
347        match lock(&self.state, "wake stop observation") {
348            Ok(state) => state.stopping,
349            Err(error) => {
350                self.refuse(&error);
351                true
352            }
353        }
354    }
355
356    pub(super) fn stop(&self) {
357        match lock(&self.state, "wake stop") {
358            Ok(mut state) => state.stopping = true,
359            Err(error) => self.refuse(&error),
360        }
361        self.changed.notify_all();
362    }
363
364    pub(super) fn wait(&self) -> bool {
365        let result = lock(&self.state, "wake wait").and_then(|state| {
366            self.changed
367                .wait_while(state, |state| !state.ready && !state.stopping)
368                .map_err(|_| failure("link_bridge_poisoned: wake wait"))
369        });
370        match result {
371            Ok(mut state) => {
372                state.ready = false;
373                !state.stopping
374            }
375            Err(error) => {
376                self.refuse(&error);
377                false
378            }
379        }
380    }
381
382    pub(super) fn check(&self) -> Result<(), ServerError> {
383        self.reason
384            .get()
385            .map_or(Ok(()), |reason| Err(failure(reason.clone())))
386    }
387
388    pub(super) fn refuse(&self, error: &ServerError) {
389        if self.reason.set(error.to_string()).is_err() {
390            return;
391        }
392        tracing::error!(%error, "link bridge stopped as a fault");
393        match self.state.lock() {
394            Ok(mut state) => state.stopping = true,
395            Err(_) => self.changed.notify_all(),
396        }
397        self.changed.notify_all();
398        if let Some(interrupt) = &self.interrupt {
399            match interrupt.shutdown(Shutdown::Both) {
400                Ok(()) => {}
401                Err(error) if error.kind() == std::io::ErrorKind::NotConnected => {
402                    tracing::info!(%error, "faulted bridge carrier already closed");
403                }
404                Err(error) => tracing::error!(%error, "faulted bridge interruption failed"),
405            }
406        }
407        if let Some(observation) = self.observation.upgrade() {
408            match observation.counters.lock() {
409                Ok(mut counters) => counters.error = Some(error.to_string()),
410                Err(_) => observation.changed.notify_all(),
411            }
412            observation.changed.notify_all();
413        }
414    }
415}