📘
Rustのbackonを使ってみた
概要
処理に失敗した場合のリトライ処理を特定のステータスコードで実行したい場合など、細かい制御を柔軟に設計したい場合にbackon crateが便利だったので、今回はそちらを紹介したいと思います。
リトライ戦略について
ここでは、リトライ戦略として使われる指数関数バックオフ(Exponential Backoff) と ジッター(Jitter)について説明します。
指数関数バックオフ
サーバーやリソースが一時的に過負荷になっている場合、リクエストを再送する間隔を徐々に長くすることで、全体の負荷を下げ、成功率を高めるための手法です。具体的には、再試行ごとに待機時間を倍々に増やしていきます。
メリットとしては、過負荷時に急激なリトライ集中を避けることができ、リトライ回数が多いほど、待機時間も長くなることでシステムを守ります。
ジッター
指数関数バックオフだけだと、複数クライアントがほぼ同時にリトライする「同期リトライのスパイク」が発生する恐れがあります。これを避けるためにランダム性(ノイズ)を加えることで、クライアント同士のリトライタイミングをずらしています。
よく使われるジッターの種類としては、下記があります。
| 戦略名 | 説明 |
|---|---|
| Full Jitter | wait_time = random(0, base * 2^n) |
| Equal Jitter | wait_time = base * 2^n / 2 + random(0, base * 2^n / 2) |
| Decorrelated Jitter |
wait_time = min(cap, random(base, previous_wait * 3))(AWS推奨) |
今回は、上記のリトライ戦略を独自に組まなくても実装できるbackonクレートを使用します。
実装例
今回は三つのURLを用意したので、順番に動かしてみます。
use backon::{ExponentialBuilder, Retryable};
use std::time::Duration;
// Define Error Type.
#[derive(Debug, thiserror::Error)]
enum SendToServerError {
#[error(transparent)]
Requests(#[from] reqwest::Error),
#[error("Retry to send request")]
Retry,
#[error("Header's Content type is not valid.")]
ContentType,
}
fn log_init() {
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info"))
.format(|buf, record| {
use std::io::Write;
writeln!(
buf,
"{} [{}] - {} - {} - {}:{}",
chrono::Utc::now().to_rfc3339(),
record.level(),
record.target(),
record.args(),
record.file().unwrap_or(""),
record.line().unwrap_or(0),
)
})
.init();
}
async fn fetch_with_retry_backon() -> Result<serde_json::Value, SendToServerError> {
let backoff = ExponentialBuilder::default()
.with_min_delay(Duration::from_millis(100))
.with_max_delay(Duration::from_millis(200))
.with_jitter()
.with_max_times(3);
// Define async process for retry as closure
let fetch_operation = || async {
log::info!("Attemping to fetch data...");
// let res = reqwest::get("https://api.example.com/data").await?;
// let res = reqwest::get("https://google.github.io/chiikawa").await?;
let res = reqwest::get("https://www.google.co.jp/index.html").await?;
let content_type = res.headers()
.get(reqwest::header::CONTENT_TYPE)
.and_then(|v| v.to_str().ok())
.unwrap_or("");
match res.status() {
reqwest::StatusCode::NOT_FOUND |
reqwest::StatusCode::INTERNAL_SERVER_ERROR => Err(SendToServerError::Retry),
_ => {
if content_type.starts_with("application/json") {
let json = res.json::<serde_json::Value>().await.map_err(SendToServerError::Requests)?;
Ok(json)
} else if content_type.starts_with("text/html") {
let html = res.text().await.map_err(SendToServerError::Requests)?;
Ok(serde_json::Value::String(html))
} else {
Err(SendToServerError::ContentType)
}
},
}
};
let response = fetch_operation
.retry(&backoff)
.sleep(tokio::time::sleep)
.when(|e| matches!(e, SendToServerError::Retry))
.await?;
Ok(response)
}
#[tokio::main]
async fn main() {
log_init();
match fetch_with_retry_backon().await {
Ok(data) => log::info!("Final Result: Successfully fetched data: {}", data),
Err(e) => log::error!("Final Result: Failed to fetch data after multiple retries: {}", e),
}
}
まずはじめに、存在しないURLを叩いてみます。
let res = reqwest::get("https://api.example.com/data").await?;
2025-07-20T05:41:31.083396+00:00 [INFO] - backon_sample - Attemping to fetch data... - src/main.rs:43
2025-07-20T05:41:31.133472+00:00 [ERROR] - backon_sample - Final Result: Failed to fetch data after multiple retries: error sending request for url (https://api.example.com/data) - src/main.rs:84
次に、下記を叩いてみます。この時、404が返ってくるので、実装例だとリトライが走ります。
let res = reqwest::get("https://google.github.io/chiikawa").await?;
2025-07-20T05:43:20.876210+00:00 [INFO] - backon_sample - Attemping to fetch data... - src/main.rs:43
2025-07-20T05:43:21.343371+00:00 [INFO] - backon_sample - Attemping to fetch data... - src/main.rs:43
2025-07-20T05:43:21.785287+00:00 [INFO] - backon_sample - Attemping to fetch data... - src/main.rs:43
2025-07-20T05:43:22.126477+00:00 [INFO] - backon_sample - Attemping to fetch data... - src/main.rs:43
2025-07-20T05:43:22.206476+00:00 [ERROR] - backon_sample - Final Result: Failed to fetch data after multiple retries: Retry to send request - src/main.rs:84
最後に、三つ目のURL(Googleの検索トップ)をたたいてみましょう。HTMLが表示されます。
let res = reqwest::get("https://www.google.co.jp/index.html").await?;
2025-07-20T05:34:55.720865+00:00 [INFO] - backon_sample - Attemping to fetch data... - src/main.rs:43
2025-07-20T05:34:56.132088+00:00 [INFO] - backon_sample - Final Result: Successfully fetched data:<HTMLのレスポンス>
まとめ
今回はリトライ機能が簡単に実装できるbackonクレートを用いた実装をしてみました。
Discussion