Skip to main content

maw/
failpoints.rs

1//! Feature-gated failpoint injection for DST.
2//!
3//! Compile with `--features failpoints` to enable injection.
4//! Without the feature, the `fp!()` macro expands to nothing.
5
6use std::collections::HashMap;
7use std::sync::{LazyLock, Mutex};
8use std::time::Duration;
9
10/// Actions a failpoint can take when triggered.
11#[derive(Clone, Debug)]
12pub enum FailpointAction {
13    /// No-op (default).
14    Off,
15    /// Return an error with the given message.
16    Error(String),
17    /// Panic with the given message.
18    Panic(String),
19    /// Abort the process.
20    Abort,
21    /// Sleep for the given duration.
22    Sleep(Duration),
23}
24
25/// Thread-safe global registry of active failpoints.
26static REGISTRY: LazyLock<Mutex<HashMap<&'static str, FailpointAction>>> =
27    LazyLock::new(|| Mutex::new(HashMap::new()));
28
29/// Set a failpoint action.
30///
31/// # Panics
32///
33/// Panics if the internal registry mutex is poisoned.
34pub fn set(name: &'static str, action: FailpointAction) {
35    REGISTRY.lock().unwrap().insert(name, action);
36}
37
38/// Clear a specific failpoint.
39///
40/// # Panics
41///
42/// Panics if the internal registry mutex is poisoned.
43pub fn clear(name: &'static str) {
44    REGISTRY.lock().unwrap().remove(name);
45}
46
47/// Clear all failpoints.
48///
49/// # Panics
50///
51/// Panics if the internal registry mutex is poisoned.
52pub fn clear_all() {
53    REGISTRY.lock().unwrap().clear();
54}
55
56/// Check if a failpoint is set and execute its action.
57/// Returns `Ok(())` if no failpoint or `Off`, `Err` if `Error` action.
58///
59/// # Panics
60///
61/// Panics if the internal registry mutex is poisoned, or if the
62/// failpoint action is `Panic`.
63pub fn check(name: &str) -> Result<(), String> {
64    let registry = REGISTRY.lock().unwrap();
65    match registry.get(name) {
66        None | Some(FailpointAction::Off) => Ok(()),
67        Some(FailpointAction::Error(msg)) => Err(msg.clone()),
68        Some(FailpointAction::Panic(msg)) => panic!("failpoint {name}: {msg}"),
69        Some(FailpointAction::Abort) => std::process::abort(),
70        Some(FailpointAction::Sleep(d)) => {
71            let d = *d;
72            drop(registry); // release lock before sleeping
73            std::thread::sleep(d);
74            Ok(())
75        }
76    }
77}
78
79/// Failpoint injection point.
80///
81/// With `failpoints` feature: checks the registry and may return `Err` or panic.
82/// Without `failpoints` feature: compiles to nothing (zero overhead).
83///
84/// Usage: `fp!("FP_COMMIT_AFTER_EPOCH_CAS")?;`
85#[cfg(feature = "failpoints")]
86#[macro_export]
87macro_rules! fp {
88    ($name:expr) => {
89        $crate::failpoints::check($name)
90            .map_err(|msg| anyhow::anyhow!("failpoint {}: {}", $name, msg))
91    };
92}
93
94#[cfg(not(feature = "failpoints"))]
95#[macro_export]
96macro_rules! fp {
97    ($name:expr) => {
98        Ok::<(), anyhow::Error>(())
99    };
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105
106    /// fp! macro is a no-op when no failpoint is set.
107    #[test]
108    fn fp_noop_when_not_set() {
109        clear_all();
110        let result = fp!("FP_TEST_NOOP");
111        assert!(result.is_ok());
112    }
113
114    /// fp! macro returns error when failpoint is set to Error.
115    #[test]
116    #[cfg(feature = "failpoints")]
117    fn fp_returns_error_when_set() {
118        clear_all();
119        set("FP_TEST_ERROR", FailpointAction::Error("injected".into()));
120        let result = fp!("FP_TEST_ERROR");
121        assert!(result.is_err());
122        let err = result.unwrap_err();
123        assert!(
124            err.to_string().contains("injected"),
125            "expected 'injected' in error: {err}"
126        );
127        clear("FP_TEST_ERROR");
128    }
129
130    /// clear_all resets all failpoints.
131    #[test]
132    fn clear_all_resets() {
133        set("FP_A", FailpointAction::Error("a".into()));
134        set("FP_B", FailpointAction::Error("b".into()));
135        clear_all();
136        assert!(fp!("FP_A").is_ok());
137        assert!(fp!("FP_B").is_ok());
138    }
139
140    /// Without the failpoints feature, fp! compiles to Ok(()).
141    /// This test validates the macro expands correctly in the current
142    /// feature configuration (it always compiles to one branch or the other).
143    #[test]
144    fn fp_compiles_to_result() {
145        clear_all();
146        let result: Result<(), anyhow::Error> = fp!("FP_COMPILE_CHECK");
147        assert!(result.is_ok());
148    }
149
150    /// Off action behaves like no failpoint set.
151    #[test]
152    #[cfg(feature = "failpoints")]
153    fn fp_off_action_is_noop() {
154        clear_all();
155        set("FP_OFF", FailpointAction::Off);
156        assert!(fp!("FP_OFF").is_ok());
157        clear("FP_OFF");
158    }
159
160    /// Sleep action returns Ok after sleeping.
161    #[test]
162    #[cfg(feature = "failpoints")]
163    fn fp_sleep_returns_ok() {
164        clear_all();
165        set(
166            "FP_SLEEP",
167            FailpointAction::Sleep(Duration::from_millis(1)),
168        );
169        assert!(fp!("FP_SLEEP").is_ok());
170        clear("FP_SLEEP");
171    }
172
173    /// clear removes a single failpoint without affecting others.
174    #[test]
175    #[cfg(feature = "failpoints")]
176    fn clear_single_failpoint() {
177        clear_all();
178        set("FP_KEEP", FailpointAction::Error("keep".into()));
179        set("FP_REMOVE", FailpointAction::Error("remove".into()));
180        clear("FP_REMOVE");
181        assert!(fp!("FP_REMOVE").is_ok());
182        assert!(fp!("FP_KEEP").is_err());
183        clear_all();
184    }
185}