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    /// Records a refused dial and answers whether its reason differs from
205    /// the last refusal's, so the retry owner tells a changed one by name
206    /// (ACCESS-009: a dial that fails after Lys issues is told, not silent).
207    pub(crate) fn dial_refused(&self, id: &str, reason: String) -> Result<bool, ServerError> {
208        let mut counters = self.snapshot().remove(id).unwrap_or_default();
209        let changed = counters.last_refusal.as_ref() != Some(&reason);
210        counters.dial_refused = counters
211            .dial_refused
212            .checked_add(1)
213            .ok_or_else(|| failure("configured dial refusal counter overflow"))?;
214        counters.last_refusal = Some(reason);
215        self.remember(id.to_owned(), counters)?;
216        Ok(changed)
217    }
218    pub(crate) fn open_refused(&self, id: &str, reason: String) -> Result<bool, ServerError> {
219        let mut counters = self.snapshot().remove(id).unwrap_or_default();
220        let changed = counters.last_refusal.as_ref() != Some(&reason);
221        counters.refused = counters
222            .refused
223            .checked_add(1)
224            .ok_or_else(|| failure("configured open refusal counter overflow"))?;
225        counters.last_refusal = Some(reason);
226        self.remember(id.to_owned(), counters)?;
227        Ok(changed)
228    }
229    #[cfg(feature = "test-support")]
230    pub(crate) fn observe_answers(
231        &self,
232        ids: &[String],
233        count: u64,
234        bound: std::time::Duration,
235    ) -> Result<bool, ServerError> {
236        let reached = |observations: &BTreeMap<String, Arc<Observation>>| {
237            ids.iter().all(|id| {
238                observations.get(id).is_some_and(|observation| {
239                    observation.settled.load(Ordering::Acquire) >= count
240                        && observation.snapshot().error.is_none()
241                })
242            })
243        };
244        let (observations, _) = self
245            .2
246            .wait_timeout_while(
247                lock(&self.0, "configured answer observation")?,
248                bound,
249                |observations| !reached(observations),
250            )
251            .map_err(|_| failure("configured answer observation poisoned"))?;
252        let observed = reached(&observations);
253        drop(observations);
254        Ok(observed)
255    }
256    pub(super) fn install(&self, id: String, observation: &Arc<Observation>) {
257        if let Err(error) = self.register_stop(&id, observation) {
258            observation.wake.refuse(&error);
259            return;
260        }
261        #[cfg(feature = "test-support")]
262        if observation
263            .registry_signal
264            .set((Arc::downgrade(&self.0), Arc::downgrade(&self.2)))
265            .is_err()
266        {
267            observation
268                .wake
269                .refuse(&failure("bridge observation registry already installed"));
270            return;
271        }
272        let result = lock(&self.0, "health registry installation").map(|mut registry| {
273            registry.insert(id, Arc::clone(observation));
274            self.2.notify_all();
275        });
276        if let Err(error) = result {
277            self.refuse(&error);
278        }
279        if let Some(reason) = self.1.reason.get() {
280            observation.wake.refuse(&failure(reason.clone()));
281        }
282    }
283    fn register_stop(&self, id: &str, observation: &Arc<Observation>) -> Result<(), ServerError> {
284        // Replacement bridges update the same stop node instead of retaining an outage's history.
285        let mut next = &self.1.stops;
286        loop {
287            if let Some(existing) = next.get() {
288                if existing.id == id {
289                    *lock(&existing.wake, "registry stop replacement")? =
290                        Arc::downgrade(&observation.wake);
291                    return Ok(());
292                }
293                next = &existing.next;
294                continue;
295            }
296            let stop = Box::new(RegistryStop {
297                id: id.to_owned(),
298                wake: Mutex::new(Arc::downgrade(&observation.wake)),
299                next: OnceLock::new(),
300            });
301            if next.set(stop).is_ok() {
302                return Ok(());
303            }
304        }
305    }
306
307    fn refuse(&self, error: &ServerError) {
308        if self.1.reason.set(error.to_string()).is_err() {
309            return;
310        }
311        tracing::error!(%error, "link bridge registry refused poisoned state");
312        let mut next = self.1.stops.get();
313        while let Some(stop) = next {
314            match lock(&stop.wake, "registry stop refusal") {
315                Ok(wake) => {
316                    if let Some(wake) = wake.upgrade() {
317                        wake.refuse(error);
318                    }
319                }
320                Err(error) => tracing::error!(%error, "registry stop refused poisoned state"),
321            }
322            next = stop.next.get();
323        }
324    }
325}
326
327#[derive(Debug, Default)]
328pub(super) struct WakeState {
329    ready: bool,
330    stopping: bool,
331}
332
333#[derive(Debug, Default)]
334pub(super) struct Wake {
335    pub(super) state: Mutex<WakeState>,
336    changed: Condvar,
337    reason: OnceLock<String>,
338    observation: Weak<Observation>,
339    interrupt: Option<TcpStream>,
340}
341
342impl Wake {
343    pub(super) fn notify(&self) {
344        match lock(&self.state, "wake notification") {
345            Ok(mut state) => state.ready = true,
346            Err(error) => self.refuse(&error),
347        }
348        self.changed.notify_one();
349    }
350
351    pub(super) fn stopping(&self) -> bool {
352        match lock(&self.state, "wake stop observation") {
353            Ok(state) => state.stopping,
354            Err(error) => {
355                self.refuse(&error);
356                true
357            }
358        }
359    }
360
361    pub(super) fn stop(&self) {
362        match lock(&self.state, "wake stop") {
363            Ok(mut state) => state.stopping = true,
364            Err(error) => self.refuse(&error),
365        }
366        self.changed.notify_all();
367    }
368
369    pub(super) fn wait(&self) -> bool {
370        let result = lock(&self.state, "wake wait").and_then(|state| {
371            self.changed
372                .wait_while(state, |state| !state.ready && !state.stopping)
373                .map_err(|_| failure("link_bridge_poisoned: wake wait"))
374        });
375        match result {
376            Ok(mut state) => {
377                state.ready = false;
378                !state.stopping
379            }
380            Err(error) => {
381                self.refuse(&error);
382                false
383            }
384        }
385    }
386
387    pub(super) fn check(&self) -> Result<(), ServerError> {
388        self.reason
389            .get()
390            .map_or(Ok(()), |reason| Err(failure(reason.clone())))
391    }
392
393    pub(super) fn refuse(&self, error: &ServerError) {
394        if self.reason.set(error.to_string()).is_err() {
395            return;
396        }
397        tracing::error!(%error, "link bridge stopped as a fault");
398        match self.state.lock() {
399            Ok(mut state) => state.stopping = true,
400            Err(_) => self.changed.notify_all(),
401        }
402        self.changed.notify_all();
403        if let Some(interrupt) = &self.interrupt {
404            match interrupt.shutdown(Shutdown::Both) {
405                Ok(()) => {}
406                Err(error) if error.kind() == std::io::ErrorKind::NotConnected => {
407                    tracing::info!(%error, "faulted bridge carrier already closed");
408                }
409                Err(error) => tracing::error!(%error, "faulted bridge interruption failed"),
410            }
411        }
412        if let Some(observation) = self.observation.upgrade() {
413            match observation.counters.lock() {
414                Ok(mut counters) => counters.error = Some(error.to_string()),
415                Err(_) => observation.changed.notify_all(),
416            }
417            observation.changed.notify_all();
418        }
419    }
420}