TiKV Rust Client 遷移記 - Futures 0.1 至 0.3

做者介紹:Nick Cameron,PingCAP 研發工程師,Rust core team 成員,專一於分佈式系統、數據庫領域和 Rust 語言的進展。html

最近我將一箇中小型的 crate 從 futures 庫的 0.1 遷移至了 0.3 版本。過程自己不是特別麻煩,但仍是有些地方或是微妙棘手,或是沒有很好的文檔說明。這篇文章裏,我會把遷移經驗總結分享給你們。git

我所遷移的 crate 是 TiKV 的 Rust Client。該 crate 的規模約爲 5500 行左右代碼,經過 gRPC 與 TiKV 交互,採用異步接口實現。所以,對於 futures 庫的使用頗爲重度。github

異步編程是 Rust 語言中影響普遍的一塊領域,已有幾年發展時間,其核心部分就是 futures 庫。做爲一個標準 Rust 庫,futures 庫爲使用 futures 編程提供所需數據類型以及功能。雖然它是異步編程的關鍵,但並不是你所須要的一切 - 你仍然須要能夠推動事件循環 (event loop) 以及與操做系統交互的其餘庫。數據庫

futures 庫在這幾年中變化很大。最新的版本爲 0.3(crates.io 發佈的 futures 預覽版)。然而,有許多早期代碼是 futures 0.1 系列版本,且一直沒有更新。這樣的分裂事出有因 - 0.1 和 0.3 版本之間變化太大。0.1 版本相對穩定,而 0.3 版本一直處於快速變化中。長遠來看,0.3 版本最終會演進爲 1.0。有一部分代碼會進入 Rust 標準庫,其中的第一部分已在最近發佈了穩定版,也就是 Future trait。編程

爲了讓 Rust Client 跑在穩定的編譯器上,咱們將核心庫限制爲僅使用穩定或即將穩定的特性。咱們在文檔和示例中確實使用了 async/await,由於 async/await 更符合工程學要求,並且未來也必定會成爲使用 Rust 進行異步編程的推薦方法。除了在覈心庫中避免使用 async/await,咱們對使用 futures 0.1 的 crate 也有依賴,這也意味着咱們須要常常用到兼容層。從這個角度說,咱們此次遷移其實並不夠典型。安全

我不是異步編程領域的專家,或許有其餘方法能讓咱們此次遷移(以及所涉及的代碼)更符合你們的使用習慣。若是您有好的建議,能夠在 Twitter 上聯繫我。若是您想要貢獻 PR 就更讚了,咱們期待愈來愈多的力量加入到 TiKV Client 項目裏。異步

機械性變化

此類變化是指那些 「查詢替換類」 ,或其餘無需複雜思考的變化。async

這一類別中最大的變化莫過於 0.1 版本的 Future 簽名中包含了一個 Error 關聯類型,並且 poll 老是會返回一個 Result。0.3 版本里該錯誤類型已被移除,對於錯誤須要顯式處理。爲了保持行爲上的一致性,咱們須要將代碼裏全部 Future<Item=Foo, Error=Bar> 替換爲 Future<Output=Result<Foo, Bar>>(留意 ItemOutput 的名稱變化)。替換後, poll 就能夠返回和之前同樣的類型,這樣在使用 futures 的時候無需任何變化。分佈式

若是你定義了本身的 futures,那就須要根據是否須要處理錯誤的需求更新 futures 的定義。異步編程

futures 0.3 中支持 TryFuture 類型,基本上能夠看做 Future<Output=Result<...>> 的替代。使用這個類型,意味着你須要在 FutureTryFuture 之間轉換,所以最好仍是儘可能避免吧。TryFuture 類型包含了一個 blanket implementation,這使它能夠經過 TryFutureEx trait 輕鬆將某些函數應用於此類 futures。

futures 0.3 中,Future::poll 方法會接受一個新的上下文參數。這基本上只須要調用 poll 方法便可完成傳遞(偶爾也會忽略)。

咱們的依賴包依然使用了 futures 0.1,因此咱們必須在兩個版本的庫之間轉換。0.3 版本包含了一些兼容層以及其餘實用工具(例如 Compat01As03)。咱們在調用依賴關係時會用到這些。

wait 方法已被從 Future trait 中移除。這是讓人拍手稱快的變化,由於該方法確實夠反人性,並且自己能夠用 .awaitexecutor::block_on 代替(須要注意的是後者可能會阻斷整個進程,而並不僅是當前執行的 future)。

Pin

futures 0.3 中, Pin 是一個頻繁使用的類型, Future::poll 方法簽名的 self 類型對其尤其青睞。除了對這些簽名進行一些機械性的處理以外,我還得藉助於 Pin::get_unchecked_mutPin::new_unchecked 這兩種方法(均爲不安全方法)對 futures 的項目字段作一些變動。

指針定位(pinning)是一個微妙又複雜的概念,我至今也不敢說本身已經掌握了多少。我能提供的最好的參考是 std::pin docs。下面是我整理的一些要點(有一些重要的細節此處不會涉及,這裏本意也並不是提供一個關於指針定位的教程)。

  • Pin 做爲一個類型構造,只有用於指針類型(如 Pin<Box<_>>)時纔會生效。

  • Pin 自己是一種「標識/封裝」類型(有一點像 NonNull),並非指針類型。

  • 若是一個指針類型被「定位」了,意味着指針指向的值不可移動(當一個非拷貝對象經過數值傳入,或者調用 mem::swap 時會發生移動)。須要注意的移動只能發生在指針被定位以前,而非以後。

  • 若是某個類型使用了 Unpin trait,這意味着不管此類型移動與否都不會有任何影響。換句話說,即便指向該類型的指針沒有被定位,咱們也能夠放心把它看成被定位的。

  • PinUnpin 並無置入 Rust 語言,雖然某些特性會對指針定位有間接依賴。指針定位由編譯器強制執行,但編譯器自己卻不自知(這點很是酷,也體現了 Rust 特性系統對此類處理的強大之處)。它是這樣工做的:Pin<P<T>> 只容許對於 P 的安全訪問,禁止移動 P 指向的任何數值,除非 T 應用了 Unpin(代碼編寫者已宣稱 T 並不在乎是否被移動)。任何容許刪除沒有執行 Unpin 數值的操做(可變訪問)都是 unsafe 的,且應該由程序編寫者決定是否要移動任何數值,並保證以後的安全代碼中不可刪除任何數值。

讓咱們回到 futures 遷移的話題上。若是你對 Pin 使用了不安全的方法,你就須要考慮上面的要點,以保證指針定位的穩定。std::pin docs 提供了更多的解釋。我在許多地方經過字段投射的方式爲另一個 future 調用 poll 方法(有時是間接的),爲了達到這個目的,你須要一個已定位的指針,這也意味着能你須要結構性指針定位。如,你能夠將 Pin<&mut T> 字段投射至 Pin<&mut FieldType>

函數

遷移中比較讓人不爽的一點是 futures 庫裏有許多函數(與類型)的名稱改變了。有的名稱和標準庫裏的通用名重複,這讓用自動化的手段處理變動的難度變大。好比,Async 變成了 PollOk 變成了 readyfor_each 變成 thenthen 變成 mapEither::A 變成 Either::Left

有時名稱沒有變化,但其表明的功能語義變了(或者兩方面都變了)。一個較爲廣泛的變化就是 closure 函數如今會返回可使用 T 類型生成數值的 future,而不會直接返回數值自己。

有許多組合子函數從 Future trait 移至擴展 crate 裏。這個問題自己不難修復,只是有時候不容易從錯誤信息中斷定。

LoopFn

0.1 版本的 futures 庫包含了 LoopFn 這個 future 構造,用於處理屢次執行某動做的 futures。LoopFn 在 0.3 版本中被移除,這樣作的緣由我的認爲多是 for 循環自己是 async 的函數,或者 streams 纔是長遠看來的更佳解決方案。爲了讓咱們的遷移過程簡單化,我爲 futures 0.3 寫了咱們本身版本的 LoopFn future,其實大部分也都是複製粘貼的工做,加上一些調整(如處理指針定位投射):code。後來我將幾處 LoopFn 用法轉換爲 streams,對代碼彷佛有必定改進。

Sink::send_all

咱們在項目中幾個地方使用了 sink。我發現對於它們對遷移和 futures 相比要有難度很多,其中最麻煩的問題就是 Sink::send_all 結構變了。0.1 版本里,Sink::send_all 會獲取 stream 的全部權,並在肯定全部 future 都完成後返回 sink 以及 stream。0.3 版本里, Sink::send_all 會接受一個對 stream 的可變引用,不返回任何值。我本身寫了一個 兼容層 在 futures 0.3 裏模擬 0.1 版本的 sink。這不是很難,但也許有更好的方式來作這件事。

你們能夠在 這個 PR 裏看到整個遷移的細節。本文最初發表在 www.ncameron.org

相關文章
相關標籤/搜索