Line data Source code
1 : //! Failpoint support code shared between pageserver and safekeepers.
2 :
3 : use tokio_util::sync::CancellationToken;
4 :
5 : /// Declare a failpoint that can use to `pause` failpoint action.
6 : /// We don't want to block the executor thread, hence, spawn_blocking + await.
7 : ///
8 : /// Optionally pass a cancellation token, and this failpoint will drop out of
9 : /// its pause when the cancellation token fires. This is useful for testing
10 : /// cases where we would like to block something, but test its clean shutdown behavior.
11 : /// The macro evaluates to a Result in that case, where Ok(()) is the case
12 : /// where the failpoint was not paused, and Err() is the case where cancellation
13 : /// token fired while evaluating the failpoint.
14 : ///
15 : /// Remember to unpause the failpoint in the test; until that happens, one of the
16 : /// limited number of spawn_blocking thread pool threads is leaked.
17 : #[macro_export]
18 : macro_rules! pausable_failpoint {
19 : ($name:literal) => {{
20 : if cfg!(feature = "testing") {
21 : let cancel = ::tokio_util::sync::CancellationToken::new();
22 : let _ = $crate::pausable_failpoint!($name, &cancel);
23 : }
24 : }};
25 : ($name:literal, $cancel:expr) => {{
26 : if cfg!(feature = "testing") {
27 : let failpoint_fut = ::tokio::task::spawn_blocking({
28 : let current = ::tracing::Span::current();
29 12625 : move || {
30 12625 : let _entered = current.entered();
31 12625 : ::tracing::info!("at failpoint {}", $name);
32 12625 : ::fail::fail_point!($name);
33 12625 : }
34 : });
35 12232 : let cancel_fut = async move {
36 12232 : $cancel.cancelled().await;
37 : };
38 : ::tokio::select! {
39 : res = failpoint_fut => {
40 : res.expect("spawn_blocking");
41 : // continue with execution
42 : Ok(())
43 : },
44 : _ = cancel_fut => {
45 : Err(())
46 : }
47 : }
48 : } else {
49 : Ok(())
50 : }
51 : }};
52 : }
53 :
54 : pub use pausable_failpoint;
55 :
56 : /// use with fail::cfg("$name", "return(2000)")
57 : ///
58 : /// The effect is similar to a "sleep(2000)" action, i.e. we sleep for the
59 : /// specified time (in milliseconds). The main difference is that we use async
60 : /// tokio sleep function. Another difference is that we print lines to the log,
61 : /// which can be useful in tests to check that the failpoint was hit.
62 : ///
63 : /// Optionally pass a cancellation token, and this failpoint will drop out of
64 : /// its sleep when the cancellation token fires. This is useful for testing
65 : /// cases where we would like to block something, but test its clean shutdown behavior.
66 : #[macro_export]
67 : macro_rules! __failpoint_sleep_millis_async {
68 : ($name:literal) => {{
69 : // If the failpoint is used with a "return" action, set should_sleep to the
70 : // returned value (as string). Otherwise it's set to None.
71 292156 : let should_sleep = (|| {
72 292156 : ::fail::fail_point!($name, |x| x);
73 292156 : ::std::option::Option::None
74 : })();
75 :
76 : // Sleep if the action was a returned value
77 : if let ::std::option::Option::Some(duration_str) = should_sleep {
78 : $crate::failpoint_support::failpoint_sleep_helper($name, duration_str).await
79 : }
80 : }};
81 : ($name:literal, $cancel:expr) => {{
82 : // If the failpoint is used with a "return" action, set should_sleep to the
83 : // returned value (as string). Otherwise it's set to None.
84 444 : let should_sleep = (|| {
85 444 : ::fail::fail_point!($name, |x| x);
86 444 : ::std::option::Option::None
87 : })();
88 :
89 : // Sleep if the action was a returned value
90 : if let ::std::option::Option::Some(duration_str) = should_sleep {
91 : $crate::failpoint_support::failpoint_sleep_cancellable_helper(
92 : $name,
93 : duration_str,
94 : $cancel,
95 : )
96 : .await
97 : }
98 : }};
99 : }
100 : pub use __failpoint_sleep_millis_async as sleep_millis_async;
101 :
102 : // Helper function used by the macro. (A function has nicer scoping so we
103 : // don't need to decorate everything with "::")
104 : #[doc(hidden)]
105 0 : pub async fn failpoint_sleep_helper(name: &'static str, duration_str: String) {
106 0 : let millis = duration_str.parse::<u64>().unwrap();
107 0 : let d = std::time::Duration::from_millis(millis);
108 0 :
109 0 : tracing::info!("failpoint {:?}: sleeping for {:?}", name, d);
110 0 : tokio::time::sleep(d).await;
111 0 : tracing::info!("failpoint {:?}: sleep done", name);
112 0 : }
113 :
114 : // Helper function used by the macro. (A function has nicer scoping so we
115 : // don't need to decorate everything with "::")
116 : #[doc(hidden)]
117 0 : pub async fn failpoint_sleep_cancellable_helper(
118 0 : name: &'static str,
119 0 : duration_str: String,
120 0 : cancel: &CancellationToken,
121 0 : ) {
122 0 : let millis = duration_str.parse::<u64>().unwrap();
123 0 : let d = std::time::Duration::from_millis(millis);
124 0 :
125 0 : tracing::info!("failpoint {:?}: sleeping for {:?}", name, d);
126 0 : tokio::time::timeout(d, cancel.cancelled()).await.ok();
127 0 : tracing::info!("failpoint {:?}: sleep done", name);
128 0 : }
129 :
130 0 : pub fn init() -> fail::FailScenario<'static> {
131 0 : // The failpoints lib provides support for parsing the `FAILPOINTS` env var.
132 0 : // We want non-default behavior for `exit`, though, so, we handle it separately.
133 0 : //
134 0 : // Format for FAILPOINTS is "name=actions" separated by ";".
135 0 : let actions = std::env::var("FAILPOINTS");
136 0 : if actions.is_ok() {
137 0 : std::env::remove_var("FAILPOINTS");
138 0 : } else {
139 0 : // let the library handle non-utf8, or nothing for not present
140 0 : }
141 :
142 0 : let scenario = fail::FailScenario::setup();
143 :
144 0 : if let Ok(val) = actions {
145 0 : val.split(';')
146 0 : .enumerate()
147 0 : .map(|(i, s)| s.split_once('=').ok_or((i, s)))
148 0 : .for_each(|res| {
149 0 : let (name, actions) = match res {
150 0 : Ok(t) => t,
151 0 : Err((i, s)) => {
152 0 : panic!(
153 0 : "startup failpoints: missing action on the {}th failpoint; try `{s}=return`",
154 0 : i + 1,
155 0 : );
156 : }
157 : };
158 0 : if let Err(e) = apply_failpoint(name, actions) {
159 0 : panic!("startup failpoints: failed to apply failpoint {name}={actions}: {e}");
160 0 : }
161 0 : });
162 0 : }
163 :
164 0 : scenario
165 0 : }
166 :
167 0 : pub fn apply_failpoint(name: &str, actions: &str) -> Result<(), String> {
168 0 : if actions == "exit" {
169 0 : fail::cfg_callback(name, exit_failpoint)
170 : } else {
171 0 : fail::cfg(name, actions)
172 : }
173 0 : }
174 :
175 : #[inline(never)]
176 0 : fn exit_failpoint() {
177 0 : tracing::info!("Exit requested by failpoint");
178 0 : std::process::exit(1);
179 : }
|