我是如何構建 backon 的?
backon[1] 是一個 Rust 錯誤重試庫,今天這篇文章旨在跟分享我在實現它的過程中一些技巧~
緣起
OpenDAL[2] 實現 RetryLayer 時需要提供一種 backoff 機制,以實現指數退避和 jitter 等特性。雖然我已經通過簡單的搜索找到了 backoff
[3],但我並不十分滿意。首先,我注意到這個庫的維護狀況似乎不太好,有 4 個未合併的 PR,而且主分支上一次更新是在 2021 年。其次,我不喜歡它提供的 API:
async fn fetch_url(url: &str) -> Result<String, reqwest::Error> {
retry(ExponentialBackoff::default(), || async { fetch().await }).await
}
backoff 的實現並不複雜,爲什麼不自己造一個用起來舒服的呢?
設計
我頭腦中第一個想法是使用 Iterator<Item = Duration>
來表示 backoff。任何能夠返回 Duration
類型的 iterator
都可以作爲 backoff
使用。使用 iterator
來表示 backoff
具有非常直接和清晰的含義,使用者可以輕鬆地理解和上手實現,而無需閱讀每個函數的註釋。其次,我希望爲 backoff 提供類似於 Rust 原生函數的使用體驗:
async fn fetch_url(url: &str) -> Result<String, reqwest::Error> {
fetch.retry(ExponentialBackoff::default()).await
}
看起來很不錯:簡單直接,不打亂用戶的閱讀順序,一眼能定位業務邏輯位置,讓我們着手實現它吧!
實現
首先,我們需要了解的是,Rust 中的 async 函數本質上都是生成器(generator)。這些生成器會捕獲當前環境的變量,並生成一個匿名的 Future。如果要重試一個 async 函數,我們需要再次調用這個生成器來生成一個全新的 Future 來執行。
我曾經走過的彎路是 Failed demo for retry: we can't retry a future directly[4] ,當時我天真地想直接重試一個 TryFuture
:
pub trait Retryable<B: Policy, F: Fn(&Self::Error) -> bool>: TryFuture + Sized {
fn retry(self, backoff: B, handle: F) -> Retry<Self, B, F>;
}
現在我明白了這種做法是錯誤的。一旦 Future
進入 Poll::Ready
狀態,我們就不應該再去輪詢它,這也正如文檔所描述的:
Once a future has completed (returned
Ready
frompoll
), calling itspoll
method again may panic, block forever, or cause other kinds of problems
接下來需要調整自己的思路,針對 || -> impl Future<Result<T>>
來實現。首先我定義了一個 Retryable trait
併爲所有的 FnMut() -> Fut
實現:
pub trait Retryable<
B: BackoffBuilder,
T,
E,
Fut: Future<Output = Result<T, E>>,
FutureFn: FnMut() -> Fut,
>
{
/// Generate a new retry
fn retry(self, builder: &B) -> Retry<B::Backoff, T, E, Fut, FutureFn>;
}
impl<B, T, E, Fut, FutureFn> Retryable<B, T, E, Fut, FutureFn> for FutureFn
where
B: BackoffBuilder,
Fut: Future<Output = Result<T, E>>,
FutureFn: FnMut() -> Fut,
{
fn retry(self, builder: &B) -> Retry<B::Backoff, T, E, Fut, FutureFn> {
Retry::new(self, builder.build())
}
}
這個 trait 涉及到一下類型參數:
-
•
B: BackoffBuilder
: 用戶傳的 backoff builder,用於指定不同的 backoff 參數 -
•
FutureFn: FnMut() -> Fut
:表示其類型是返回 Fut 的函數 -
•
FnOnce
要求 take ownership,不能多次調用 -
• 而
Fn
只能拿到&self
引用,很多場景下使用會受限 -
•
Fut: Future<Output = Result<T, E>>
:這表示一個 Future,它返回的類型是Result<T, E>
返回的 Retry
結構體則包裝了上述這些所有類型:
pub struct Retry<B: Backoff, T, E, Fut: Future<Output = Result<T, E>>, FutureFn: FnMut() -> Fut> {
backoff: B,
retryable: fn(&E) -> bool,
notify: fn(&E, Duration),
future_fn: FutureFn,
#[pin]
state: State<T, E, Fut>,
}
除了 backoff
和 future_fn
之外,我們引入了 retryable
和 notify
用來實現 retryable error 檢查和通知功能。類型系統想清楚之後,接下來的工作就是給 Retry 實現正確的 Future
trait 了,細節不再贅述:
impl<B, T, E, Fut, FutureFn> Future for Retry<B, T, E, Fut, FutureFn>
where
B: Backoff,
Fut: Future<Output = Result<T, E>>,
FutureFn: FnMut() -> Fut,
{
type Output = Result<T, E>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
...
}
}
此外,還有一些事務性的工作需要完成:我們需要讓用戶定義哪些 Error 是可以進行重試的,並且需要提供自定義通知重試的功能。
最後組合起來的效果如下:
#[tokio::main]
async fn main() -> Result<()> {
let content = fetch
.retry(&ExponentialBuilder::default())
.when(|e| e.to_string() == "EOF")
.notify(|err, dur| {
println!("retrying error {:?} with sleeping {:?}", err, dur);
})
.await?;
Ok(())
}
看起來很完美!
One More Thing
哦,等一等,backon 還不支持同步函數!沒關係,我們只需要應用相同的思路:
pub trait BlockingRetryable<B: BackoffBuilder, T, E, F: FnMut() -> Result<T, E>> {
/// Generate a new retry
fn retry(self, builder: &B) -> BlockingRetry<B::Backoff, T, E, F>;
}
impl<B, T, E, F> BlockingRetryable<B, T, E, F> for F
where
B: BackoffBuilder,
F: FnMut() -> Result<T, E>,
{
fn retry(self, builder: &B) -> BlockingRetry<B::Backoff, T, E, F> {
BlockingRetry::new(self, builder.build())
}
}
由於 fn_traits[5] 特性還沒有 stable,所以我選擇給 BlockingRetry 增加了一個新的函數:
impl<B, T, E, F> BlockingRetry<B, T, E, F>
where
B: Backoff,
F: FnMut() -> Result<T, E>,
{
pub fn call(mut self) -> Result<T, E> {
...
}
}
在 call 中完成重試的操作,用起來感覺也很不錯,跟 Async 的版本有一種相呼應的美感。
fn main() -> Result<()> {
let content = fetch
.retry(&ExponentialBuilder::default())
.when(|e| e.to_string() == "EOF")
.notify(|err, dur| {
println!("retrying error {:?} with sleeping {:?}", err, dur);
})
.call()?;
Ok(())
}
總結
在本文中,我分享了 backon
的設計和具體實現。在這個過程中,我主要使用了 Rust 的泛型機制,分別爲 FnMut() -> Fut
和 FnMut() -> Result<T, E>
來實現了自定義 trait 來增加新的功能。我希望這個實現能夠啓發大家設計更加用戶友好的庫 API。
感謝大家的閱讀!
引用鏈接
[1]
backon: https://github.com/Xuanwo/backon
[2]
OpenDAL: https://github.com/datafuselabs/opendal
[3]
backoff
: https://github.com/ihrwein/backoff
[4]
Failed demo for retry: we can't retry a future directly: https://github.com/Xuanwo/backon/pull/1
[5]
fn_traits: https://github.com/rust-lang/rust/issues/29625
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/N5jmCOoqXwXyIFmNahmPBA