[Rust] シンプルなリトライ処理をする[Backon]
Introduction
処理に失敗したら自動でリトライしてほしいときがけっこうあります。
ライブラリ/フレームワークではけっこう実装されてたりしますが、
今回は自前で手軽に実装する方法とBackon crateを使う方法を紹介します。
Environment
- MacBook Pro (14-inch, M3, 2023)
- OS : MacOS 14.5
- Rust : 1.81.0
Try
シンプルな実装
最初に自分でリトライ処理を実装してみます。
cargo newでプロジェクトを作成して下記コードを記述してみましょう。
use std::thread;
use std::time::Duration;
// F: 実行する操作の型
// T: 操作が成功した場合の戻り値の型
// E: 操作が失敗した場合のエラーの型
fn retry<F, T, E>(mut op: F, max_attempts: u32, delay: Duration) -> Result<T, E>
where
// F は Result<T, E> を返す関数
F: FnMut() -> Result<T, E>,
{
let mut attempts = 0;
loop {
match op() {
Ok(result) => return Ok(result),
Err(error) => {
attempts += 1;
// 最大試行回数に達した場合、エラー
if attempts >= max_attempts {
return Err(error);
}
println!("{} times failed, retrying after {:?}...", attempts, delay);
// 指定された時間だけスレッドをスリープ
thread::sleep(delay);
}
}
}
}
fn main() {
let result = retry(
// 再試行したい処理をクロージャとして定義
|| {
// ランダムにtrue/falseを返す
if rand::random() {
Ok("Success")
} else {
Err("Failed")
}
},
3, // 最大試行回数
Duration::from_secs(1) // 再試行間隔(1秒)
);
match result {
//成功
Ok(value) => println!("Operation succeeded: {}", value),
// 全ての試行が失敗
Err(error) => println!("Operation failed after 3 attempts: {}", error),
}
}
loopをつかってリトライを実装してみました。
Resultを返す関数をクロージャで指定しています。
実行すると下記のようにリトライします。
シンプルでわかりやすいです。
% cargo run
1 times failed, retrying after 1s...
2 times failed, retrying after 1s...
Operation succeeded: Success
BackOn
BackONはRustのリトライライブラリです。
リトライのデファクトを目指しているとのことです。
非同期関数もサポートし、リトライ時の細かい制御が可能です。
リトライ戦略を選択して使用することもできます。
※指数やフィボナッチ数列に基づいたバックオフ戦略
また、WASM互換性もありno_stdもサポートありといい感じです。
今後はtowerやreqwestなどのcrateに統合される計画もあります。
Try
ではBackonを試してみます。
backon crateを追加してから作業開始しましょう。
% cd path/your/project
% cargo add backon
#その他使用するcrateも追加
% cargo add anyhow
% carog add tokio
下記Rustコードを記述してみましょう。
fetch関数を定義してリトライしてみます。
use anyhow::{Result, anyhow};
use backon::ExponentialBuilder;
use backon::FibonacciBuilder;
use backon::Retryable;
use tokio::time::Duration;
use std::sync::atomic::{AtomicUsize, Ordering};
static ATTEMPT_COUNT: AtomicUsize = AtomicUsize::new(0);
async fn fetch() -> Result<String> {
let attempt = ATTEMPT_COUNT.fetch_add(1, Ordering::SeqCst);
if attempt == 0 {
// 最初は失敗
Err(anyhow!("EOF"))
} else {
// 2回目以降の試行では成功
Ok("hello, world!".to_string())
}
}
#[tokio::main]
async fn main() -> Result<()> {
//指数遅延によるバックオフ
let retry_policy = ExponentialBuilder::default()
.with_min_delay(Duration::from_millis(10000)) // 最小遅延時間
.with_max_delay(Duration::from_secs(50000)) // 最大遅延時間
.with_factor(2.0) // 遅延時間の増加係数
.with_max_times(5); // 最大リトライ回数
//フィボナッチ遅延によるバックオフ
let fibonacci_retry_policy = FibonacciBuilder::default()
.with_min_delay(Duration::from_millis(1000)) // 最小遅延時間
.with_max_delay(Duration::from_secs(5000)) // 最大遅延時間
.with_jitter() // ジッター(ランダムな変動)を追加
.with_max_times(5); // 最大リトライ回数
let content = fetch
.retry(retry_policy)
//.retry(fibonacci_retry_policy)
.sleep(tokio::time::sleep)
.when(|e| e.to_string() == "EOF")
.notify(|err: &anyhow::Error, dur: Duration| {
println!("retrying {:?} after {:?}", err, dur);
})
.await?;
println!("fetch succeeded: {}", content);
Ok(())
}
fetch関数は最初の呼び出しで失敗し、その後の呼び出しで成功するように実装します。
Backonクレートを使用して、指数バックオフかフィボナッチバックオフの
リトライを実装しています。
また、エラーが"EOF"の場合にリトライし、各試行間の待機時間を設定できます。
ここではExponentialBuilderとFibonacciBuilderを使っていますが、
他にConstantBuilder(一定の遅延でバックオフ)もあります。
FibonacciBuilderは、フィボナッチ遅延によるバックオフを行うのですが、
リトライの間隔がフィボナッチ数列に従って増加していきます。
3回目以降のリトライがフィボナッチ数列に従って計算された時間後に行われます。
そして、ジッター(jitter)が有効な場合、計算された遅延時間にランダム時間が追加されます。
※複数クライアントが同時にリトライする場合の処理
設定された最大リトライ回数(max_times)に達すると、それ以上リトライは行われません。
% cargo run
retrying EOF after 1.931672335s
fetch succeeded: hello, world!
簡単にリトライが実装できました。