Skip to content

Commit 2892f6e

Browse files
committed
feat: callback_with_output
This changeset introduces callback_with_output, which is a way to use a callback at runtime to determine what parameter to pass to a potential failpoint. Signed-off-by: o0Ignition0o <[email protected]>
1 parent 4bf6fc9 commit 2892f6e

File tree

2 files changed

+128
-0
lines changed

2 files changed

+128
-0
lines changed

src/lib.rs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,34 @@ impl SyncCallback {
260260
}
261261
}
262262

263+
#[derive(Clone)]
264+
struct SyncCallbackWithOutput(Arc<dyn Fn() -> Option<Option<String>> + Send + Sync>);
265+
266+
impl Debug for SyncCallbackWithOutput {
267+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
268+
f.write_str("SyncCallbackWithOutput()")
269+
}
270+
}
271+
272+
impl PartialEq for SyncCallbackWithOutput {
273+
fn eq(&self, other: &Self) -> bool {
274+
Arc::ptr_eq(&self.0, &other.0)
275+
}
276+
}
277+
278+
impl SyncCallbackWithOutput {
279+
fn new(
280+
f: impl Fn() -> Option<Option<String>> + Send + Sync + 'static,
281+
) -> SyncCallbackWithOutput {
282+
SyncCallbackWithOutput(Arc::new(f))
283+
}
284+
285+
fn run(&self) -> Option<Option<String>> {
286+
let callback = &self.0;
287+
callback()
288+
}
289+
}
290+
263291
/// Supported tasks.
264292
#[derive(Clone, Debug, PartialEq)]
265293
enum Task {
@@ -281,6 +309,8 @@ enum Task {
281309
Delay(u64),
282310
/// Call callback function.
283311
Callback(SyncCallback),
312+
/// Call callback function, and pass output to the fail_point expression.
313+
CallbackWithOutput(SyncCallbackWithOutput),
284314
}
285315

286316
#[derive(Debug)]
@@ -324,6 +354,17 @@ impl Action {
324354
}
325355
}
326356

357+
fn from_callback_with_output(
358+
f: impl Fn() -> Option<Option<String>> + Send + Sync + 'static,
359+
) -> Action {
360+
let task = Task::CallbackWithOutput(SyncCallbackWithOutput::new(f));
361+
Action {
362+
task,
363+
freq: 1.0,
364+
count: None,
365+
}
366+
}
367+
327368
fn get_task(&self) -> Option<Task> {
328369
use rand::Rng;
329370

@@ -508,6 +549,7 @@ impl FailPoint {
508549
Task::Callback(f) => {
509550
f.run();
510551
}
552+
Task::CallbackWithOutput(f) => return f.run(),
511553
}
512554
None
513555
}
@@ -696,6 +738,28 @@ where
696738
Ok(())
697739
}
698740

741+
/// Configure the actions for a fail point at runtime.
742+
///
743+
/// Each fail point can be configured by a callback. Process will call this callback function
744+
/// when it meet this fail-point.
745+
/// Its output will be used as the expression parameter for the `fail_point!` macro.
746+
///
747+
/// Refer to the `test_callback_with_output` test for more information.
748+
pub fn cfg_callback_with_output<S, F>(name: S, f: F) -> Result<(), String>
749+
where
750+
S: Into<String>,
751+
F: Fn() -> Option<Option<String>> + Send + Sync + 'static,
752+
{
753+
let mut registry = REGISTRY.registry.write().unwrap();
754+
let p = registry
755+
.entry(name.into())
756+
.or_insert_with(|| Arc::new(FailPoint::new()));
757+
let action = Action::from_callback_with_output(f);
758+
let actions = vec![action];
759+
p.set_actions("callback_with_output", actions);
760+
Ok(())
761+
}
762+
699763
/// Remove a fail point.
700764
///
701765
/// If the fail point doesn't exist, nothing will happen.

tests/tests.rs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,70 @@ fn test_callback() {
160160
assert_eq!(2, counter.load(Ordering::SeqCst));
161161
}
162162

163+
#[test]
164+
#[cfg_attr(not(feature = "failpoints"), ignore)]
165+
fn test_callback_with_output() {
166+
let return_42 = || {
167+
fail_point!("cb_with_output", |output_from_callback: Option<String>| {
168+
if let Some(output) = output_from_callback {
169+
let as_i32 = output.parse().expect("invalid integer");
170+
// return the number that was passed
171+
as_i32
172+
} else {
173+
// No output was passed by the callback,
174+
// return an arbitrary number
175+
84
176+
}
177+
});
178+
// default function behavior
179+
42
180+
};
181+
182+
let counter = Arc::new(AtomicUsize::new(0));
183+
let counter2 = counter.clone();
184+
185+
// We haven't set up the callback yet,
186+
// the function will behave as usual
187+
assert_eq!(42, return_42());
188+
189+
// Configure the callback to return the counter,
190+
// Only if counter can be divided by two
191+
fail::cfg_callback_with_output("cb_with_output", move || {
192+
let prev = counter2.fetch_add(1, Ordering::SeqCst);
193+
194+
if prev == 0 {
195+
// First call, we decide to not return anything
196+
return None;
197+
}
198+
199+
if prev % 2 == 0 {
200+
Some(Some(prev.to_string()))
201+
} else {
202+
Some(None)
203+
}
204+
})
205+
.unwrap();
206+
207+
// Fist call via the callback,
208+
// The callback must have passed `None`
209+
// to the fail point,
210+
// Thus not triggering it.
211+
assert_eq!(42, return_42());
212+
// Second call via the callback,
213+
// which returned Some(None)
214+
// We entered the "arbitrary number" branch
215+
assert_eq!(84, return_42());
216+
// Third call via the callback,
217+
// which returned Some(Some("2".to_string()))
218+
// We entered the "return passed number" branch
219+
assert_eq!(2, return_42());
220+
221+
// The callback has always been called,
222+
// it was responsible for determining the parameter
223+
// passed to the failpoint
224+
assert_eq!(3, counter.load(Ordering::SeqCst));
225+
}
226+
163227
#[test]
164228
#[cfg_attr(not(feature = "failpoints"), ignore)]
165229
fn test_delay() {

0 commit comments

Comments
 (0)