atlassian-cli-api 0.7.0

API client library for Atlassian Cloud products
Documentation
use crate::error::{ApiError, Result};
use backoff::{backoff::Backoff, ExponentialBackoff};
use std::time::Duration;
use tracing::{debug, warn};

#[derive(Clone, Debug)]
pub struct RetryConfig {
    pub max_retries: usize,
    pub initial_interval: Duration,
    pub max_interval: Duration,
    pub multiplier: f64,
}

impl Default for RetryConfig {
    fn default() -> Self {
        Self {
            max_retries: 3,
            initial_interval: Duration::from_millis(500),
            max_interval: Duration::from_secs(30),
            multiplier: 2.0,
        }
    }
}

impl RetryConfig {
    pub fn backoff(&self) -> ExponentialBackoff {
        ExponentialBackoff {
            current_interval: self.initial_interval,
            initial_interval: self.initial_interval,
            randomization_factor: 0.1,
            multiplier: self.multiplier,
            max_interval: self.max_interval,
            max_elapsed_time: None,
            ..Default::default()
        }
    }
}

pub async fn retry_with_backoff<F, Fut, T>(config: &RetryConfig, operation: F) -> Result<T>
where
    F: Fn() -> Fut,
    Fut: std::future::Future<Output = Result<T>>,
{
    let mut backoff = config.backoff();
    let mut attempts = 0;

    loop {
        attempts += 1;
        debug!(attempt = attempts, "Executing request");

        match operation().await {
            Ok(result) => {
                if attempts > 1 {
                    debug!(attempts, "Request succeeded after retries");
                }
                return Ok(result);
            }
            Err(err) if err.is_retryable() && attempts < config.max_retries => {
                if let Some(wait) = backoff.next_backoff() {
                    warn!(
                        error = %err,
                        attempt = attempts,
                        wait_ms = wait.as_millis(),
                        "Request failed, retrying"
                    );
                    tokio::time::sleep(wait).await;
                } else {
                    return Err(ApiError::Timeout { attempts });
                }
            }
            Err(err) => {
                if attempts >= config.max_retries {
                    warn!(attempts, "Max retries exceeded");
                }
                return Err(err);
            }
        }
    }
}