📘

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