做者:張博康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。