use std::time::Duration;
use tracing::{debug, warn};
use crate::collect::errors::{CollectError, Result};
use crate::collect::github::budget::{rate_limit_delay, FetchBudget};
pub(crate) const MAX_RETRIES: u32 = 3;
pub(crate) const RETRY_BASE_MS: u64 = 1000;
fn backoff(attempt: u32) -> Duration {
Duration::from_millis(RETRY_BASE_MS * (1u64 << attempt))
}
pub(crate) async fn retry_get(
client: &reqwest::Client,
url: &str,
budget: &FetchBudget,
) -> Result<reqwest::Response> {
debug!(url = %url, "GET (with retry)");
retry_send(client.get(url), budget).await
}
pub(crate) async fn retry_send(
req: reqwest::RequestBuilder,
budget: &FetchBudget,
) -> Result<reqwest::Response> {
if let Some(e) = budget.tripped_error() {
return Err(e);
}
let mut last_err: Option<reqwest::Error> = None;
for attempt in 0..=MAX_RETRIES {
let Some(this_attempt) = req.try_clone() else {
return req.send().await.map_err(CollectError::Http);
};
match this_attempt.send().await {
Ok(resp) => {
let status = resp.status().as_u16();
if let Some(delay) = rate_limit_delay(status, resp.headers()) {
if attempt == MAX_RETRIES {
warn!(
status,
attempt,
"GitHub is still rate-limiting after the attempt cap; \
stopping GitHub collection for this run"
);
return Err(budget.trip(status, Some(delay)));
}
let delay = budget.reserve(delay, status)?;
warn!(
status,
attempt,
delay_ms = delay.as_millis() as u64,
"GitHub rate-limited the request; waiting out Retry-After"
);
tokio::time::sleep(delay).await;
continue;
}
let transient = (500..=599).contains(&status);
if !transient || attempt == MAX_RETRIES {
return Ok(resp);
}
let delay = budget.reserve(backoff(attempt), status)?;
warn!(
status,
attempt,
delay_ms = delay.as_millis() as u64,
"GitHub returned a transient status; retrying"
);
tokio::time::sleep(delay).await;
}
Err(e) => {
if attempt == MAX_RETRIES {
return Err(CollectError::Http(e));
}
let delay = budget.reserve(backoff(attempt), 0)?;
warn!(error = %e, attempt, delay_ms = delay.as_millis() as u64,
"transport error; retrying");
last_err = Some(e);
tokio::time::sleep(delay).await;
}
}
}
Err(CollectError::Http(
last_err.expect("retry loop preserved error"),
))
}
#[cfg(test)]
#[path = "retry_tests.rs"]
mod retry_tests;