Skip to main content

precept/
fault.rs

1use std::{
2    collections::HashSet,
3    fmt::Debug,
4    sync::atomic::{AtomicBool, AtomicU32, Ordering},
5};
6
7use crate::ENABLED;
8
9#[cfg(feature = "enabled")]
10#[doc(hidden)]
11#[linkme::distributed_slice]
12pub static FAULT_CATALOG: [FaultEntry];
13
14#[cfg(not(feature = "enabled"))]
15#[doc(hidden)]
16pub static FAULT_CATALOG: [&FaultEntry; 0] = [];
17
18pub(crate) fn init_faults() {
19    let mut seen = HashSet::new();
20    for entry in FAULT_CATALOG {
21        // fail if we have already seen this entry
22        if !seen.insert(entry.name) {
23            panic!("Duplicate Precept fault: {}", entry.name);
24        }
25    }
26}
27
28/// A fault injection point that can be triggered during testing.
29///
30/// Faults can be enabled/disabled and can be forced to trigger a specific
31/// number of times using the pending trips mechanism.
32#[derive(Debug)]
33pub struct FaultEntry {
34    /// the name of the fault, also serves as its Catalog id
35    name: &'static str,
36
37    /// whether or not this fault is enabled
38    enabled: AtomicBool,
39
40    /// if this value is > 0, the next call to `trip` will return true and this
41    /// value will be decremented
42    pending_trips: AtomicU32,
43}
44
45impl FaultEntry {
46    /// Creates a new fault entry with the given name.
47    pub const fn new(name: &'static str) -> Self {
48        Self {
49            name,
50            enabled: AtomicBool::new(true),
51            pending_trips: AtomicU32::new(0),
52        }
53    }
54
55    /// Returns true when the fault should trip
56    pub fn trip(&self) -> bool {
57        if self
58            .pending_trips
59            .fetch_update(Ordering::AcqRel, Ordering::Acquire, |count| {
60                if count > 0 { Some(count - 1) } else { None }
61            })
62            .is_ok()
63        {
64            // forced trigger
65            true
66        } else if self.enabled.load(Ordering::Acquire) {
67            let should_fault = crate::dispatch::choose(&[true, false]);
68            should_fault.is_some_and(|&t| t)
69        } else {
70            false
71        }
72    }
73
74    /// Enables this fault, allowing it to trip.
75    pub fn enable(&self) {
76        self.enabled.store(true, Ordering::Release);
77    }
78
79    /// Disables this fault, preventing it from tripping.
80    pub fn disable(&self) {
81        self.enabled.store(false, Ordering::Release);
82    }
83
84    /// Sets the number of pending forced trips.
85    ///
86    /// When pending trips are set, the next `count` calls to [`trip`](Self::trip)
87    /// will return `true` regardless of random chance.
88    pub fn set_pending(&self, count: u32) {
89        self.pending_trips.store(count, Ordering::Release);
90    }
91
92    /// Returns the number of pending forced trips remaining.
93    pub fn count_pending(&self) -> u32 {
94        self.pending_trips.load(Ordering::Acquire)
95    }
96}
97
98/// Enables all registered faults.
99///
100/// Panics if precept is disabled.
101pub fn enable_all() {
102    assert!(ENABLED, "Precept is disabled");
103    for entry in FAULT_CATALOG {
104        entry.enable()
105    }
106}
107
108/// Disables all registered faults.
109pub fn disable_all() {
110    tracing::warn!("Precept Faults disabled");
111    for entry in FAULT_CATALOG {
112        entry.disable();
113    }
114}
115
116/// Looks up a fault entry by its name.
117///
118/// Returns `None` if no fault with the given name exists.
119pub fn get_fault_by_name(name: &str) -> Option<&'static FaultEntry> {
120    FAULT_CATALOG.into_iter().find(|&entry| entry.name == name)
121}