use std::future::Future;
use std::time::Duration;
use crate::provider::ProviderError;
#[derive(Debug)]
pub struct HttpFailure {
pub error: ProviderError,
pub force_terminal: bool,
pub retry_after: Option<Duration>,
}
impl HttpFailure {
pub fn transport(message: impl Into<String>) -> Self {
Self {
error: ProviderError::Transport(message.into()),
force_terminal: false,
retry_after: None,
}
}
pub fn decode(message: impl Into<String>) -> Self {
Self {
error: ProviderError::Decode(message.into()),
force_terminal: false,
retry_after: None,
}
}
}
#[must_use]
pub fn parse_retry_after(value: &str) -> Option<Duration> {
value.trim().parse::<u64>().ok().map(Duration::from_secs)
}
#[derive(Debug, Clone)]
pub struct RetryPolicy {
pub max_attempts: u32,
pub base_delay: Duration,
pub max_delay: Duration,
pub rate_limit_attempts: u32,
pub retry_after_cap: Duration,
}
impl Default for RetryPolicy {
fn default() -> Self {
Self {
max_attempts: 8,
base_delay: Duration::from_millis(500),
max_delay: Duration::from_secs(32),
rate_limit_attempts: 2,
retry_after_cap: Duration::from_mins(2),
}
}
}
#[must_use]
pub fn backoff(policy: &RetryPolicy, attempt: u32, retry_after: Option<Duration>) -> Duration {
if let Some(after) = retry_after {
return after.min(policy.retry_after_cap);
}
let exp = policy
.base_delay
.saturating_mul(2u32.saturating_pow(attempt.saturating_sub(1)))
.min(policy.max_delay);
let jitter: f64 = rand::Rng::random_range(&mut rand::rng(), 0.9..1.1);
exp.mul_f64(jitter)
}
pub async fn run_with_retry<T, F, Fut>(policy: &RetryPolicy, mut op: F) -> Result<T, ProviderError>
where
F: FnMut(u32) -> Fut,
Fut: Future<Output = Result<T, HttpFailure>>,
{
let mut rate_limit_hits: u32 = 0;
let mut attempt: u32 = 0;
loop {
attempt += 1;
let failure = match op(attempt).await {
Ok(value) => return Ok(value),
Err(failure) => failure,
};
if failure.force_terminal || !failure.error.retryable() {
return Err(failure.error);
}
if matches!(failure.error, ProviderError::RateLimited { .. }) {
rate_limit_hits += 1;
if rate_limit_hits > policy.rate_limit_attempts {
return Err(failure.error);
}
}
if attempt >= policy.max_attempts {
return Err(failure.error);
}
tokio::time::sleep(backoff(policy, attempt, failure.retry_after)).await;
}
}
pub fn build_http_client() -> Result<reqwest::Client, ProviderError> {
reqwest::Client::builder()
.connect_timeout(Duration::from_secs(30))
.timeout(Duration::from_mins(10))
.build()
.map_err(|e| ProviderError::Transport(format!("client construction: {e}")))
}
#[must_use]
pub fn normalize_input_schema(mut schema: serde_json::Value) -> serde_json::Value {
if let Some(obj) = schema.as_object_mut() {
obj.remove("$schema");
}
schema
}