做者:张博康git
本文为 TiKV 源码解析系列的第五篇,为你们介绍 TiKV 在测试中使用的周边库 fail-rs。github
fail-rs 的设计启发于 FreeBSD 的 failpoints,由 Rust 实现。经过代码或者环境变量,其容许程序在特定的地方动态地注入错误或者其余行为。在 TiKV 中一般在测试中使用 fail point 来构建异常的状况,是一个很是方便的测试工具。shell
在咱们的集成测试中,都是简单的构建一个 KV 实例,而后发送请求,检查返回值和状态的改变。这样的测试能够较为完整地测试功能,可是对于一些须要精细化控制的测试就鞭长莫及了。咱们固然能够经过 mock 网络层提供网络的精细模拟控制,可是对于诸如磁盘 IO、系统调度等方面的控制就没办法作到了。数据库
同时,在分布式系统中时序的关系是很是关键的,可能两个操做的执行顺行相反,就致使了迥然不一样的结果。尤为对于数据库来讲,保证数据的一致性是相当重要的,所以须要去作一些相关的测试。网络
基于以上缘由,咱们就须要使用 fail point 来复现一些 corner case,好比模拟数据落盘特别慢、raftstore 繁忙、特殊的操做处理顺序、错误 panic 等等。闭包
在详细介绍以前,先举一个简单的例子给你们一个直观的认识。分布式
仍是那个老生常谈的 Hello World:函数
#[macro_use] extern crate fail; fn say_hello() { fail_point!(“before_print”); println!(“Hello World~”); } fn main() { say_hello(); fail::cfg("before_print", "panic"); say_hello(); }
运行结果以下:工具
Hello World~ thread 'main' panicked at 'failpoint before_print panic' ...
能够看到最终只打印出一个 Hello World~
,而在打印第二个以前就 panic 了。这是由于咱们在第一次打印完后才指定了这个 fail point 行为是 panic,所以第一次在 fail point 不作任何事情以后正常输出,而第二次在执行到 fail point 时就会根据配置的行为 panic 掉!测试
固然 fail point 不只仅能注入 panic,还能够是其余的操做,而且能够按照必定的几率出现。描述行为的格式以下:
[<pct>%][<cnt>*]<type>[(args...)][-><more terms>]
type:行为类型
好比咱们想在 before_print
处先 sleep 1s 而后有 1% 的机率 panic,那么就能够这么写:
"sleep(1000)->1%panic"
只须要使用宏 fail_point!
就能够在相应代码中提早定义好 fail point,而具体的行为在以后动态注入。
fail_point!("failpoint_name"); fail_point!("failpoint_name", |_| { // 指定生成自定义返回值的闭包,只有当 fail point 的行为为 return 时,才会调用该闭包并返回结果 return Error }); fail_point!("failpoint_name", a == b, |_| { // 当知足条件时,fail point 才被触发 return Error })
经过设置环境变量指定相应 fail point 的行为:
FAILPOINTS="<failpoint_name1>=<action>;<failpoint_name2>=<action>;..."
注意,在实际运行的代码须要先使用 fail::setup()
以环境变量去设置相应 fail point,不然 FAILPOINTS
并不会起做用。
#[macro_use] extern crate fail; fn main() { fail::setup(); // 初始化 fail point 设置 do_fallible_work(); fail::teardown(); // 清除全部 fail point 设置,而且恢复全部被 fail point 暂停的线程 }
不一样于环境变量方式,代码控制更加灵活,能够在程序中根据状况动态调整 fail point 的行为。这种方式主要应用于集成测试,以此能够很轻松地构建出各类异常状况。
fail::cfg("failpoint_name", "actions"); // 设置相应的 fail point 的行为 fail::remove("failpoint_name"); // 解除相应的 fail point 的行为
如下咱们将以 fail-rs v0.2.1 版本代码为基础,从 API 出发来看看其背后的具体实现。
fail-rs 的实现很是简单,总的来讲,就是内部维护了一个全局 map,其保存着相应 fail point 所对应的行为。当程序执行到某个 fail point 时,获取并执行该全局 map 中所保存的相应的行为。
全局 map 其具体定义在 FailPointRegistry。
struct FailPointRegistry { registry: RwLock<HashMap<String, Arc<FailPoint>>>, }
其中 FailPoint 的定义以下:
struct FailPoint { pause: Mutex<bool>, pause_notifier: Condvar, actions: RwLock<Vec<Action>>, actions_str: RwLock<String>, }
pause
和 pause_notifier
是用于实现线程的暂停和恢复,感兴趣的同窗能够去看看代码,太过细节在此不展开了;actions_str
保存着描述行为的字符串,用于输出;而 actions
就是保存着 failpoint 的行为,包括几率、次数、以及具体行为。Action
实现了 FromStr
的 trait,能够将知足格式要求的字符串转换成 Action
。这样各个 API 的操做也就显而易见了,实际上就是对于这个全局 map 的增删查改:
FAILPOINTS
的值,以 ;
分割,解析出多个 failpoint name
和相应的 actions
并保存在 registry
中。registry
中全部 fail point 对应的 actions
为空。name
和对应解析出的 actions
保存在 registry
中。registry
中 name
对应的 actions
为空。而代码到执行到 fail point 的时候到底发生了什么呢,咱们能够展开 fail_point! 宏定义看一下:
macro_rules! fail_point { ($name:expr) => {{ $crate::eval($name, |_| { panic!("Return is not supported for the fail point \"{}\"", $name); }); }}; ($name:expr, $e:expr) => {{ if let Some(res) = $crate::eval($name, $e) { return res; } }}; ($name:expr, $cond:expr, $e:expr) => {{ if $cond { fail_point!($name, $e); } }}; }
如今一切都变得豁然开朗了,实际上就是对于 eval
函数的调用,当函数返回值为 Some
时则提早返回。而 eval
就是从全局 map 中获取相应的行为,在 p.eval(name)
中执行相应的动做,好比输出、等待亦或者 panic。而对于 return
行为的状况会特殊一些,在 p.eval(name)
中并不作实际的动做,而是返回 Some(arg)
并经过 .map(f)
传参给闭包产生自定义的返回值。
pub fn eval<R, F: FnOnce(Option<String>) -> R>(name: &str, f: F) -> Option<R> { let p = { let registry = REGISTRY.registry.read().unwrap(); match registry.get(name) { None => return None, Some(p) => p.clone(), } }; p.eval(name).map(f) }
至此,关于 fail-rs 背后的秘密也就清清楚楚了。关于在 TiKV 中使用 fail point 的测试详见 github.com/tikv/tikv/tree/master/tests/failpoints,你们感兴趣能够看看在 TiKV 中是如何来构建异常状况的。
同时,fail-rs 计划支持 HTTP API,欢迎感兴趣的小伙伴提交 PR。