use std::sync::Mutex;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use reqwest::header::{HeaderMap, RETRY_AFTER};
use crate::collect::errors::CollectError;
pub(crate) const MAX_PAGES: u32 = 100;
pub(crate) const MAX_REFERENCE_LOOKUPS: usize = 500;
pub(crate) const RATE_LIMIT_SLEEP_BUDGET: Duration = Duration::from_secs(120);
pub(crate) const MAX_RETRY_AFTER: Duration = Duration::from_secs(60);
pub(crate) const DEFAULT_RATE_LIMIT_DELAY: Duration = Duration::from_secs(1);
pub(crate) fn rate_limit_delay(status: u16, headers: &HeaderMap) -> Option<Duration> {
let retry_after = header_u64(headers, RETRY_AFTER.as_str()).map(Duration::from_secs);
let quota_drained = header_u64(headers, "x-ratelimit-remaining") == Some(0);
let rate_limited = status == 429 || (status == 403 && (retry_after.is_some() || quota_drained));
if !rate_limited {
return None;
}
let delay = retry_after
.or_else(|| reset_delay(headers))
.unwrap_or(DEFAULT_RATE_LIMIT_DELAY);
Some(delay.min(MAX_RETRY_AFTER))
}
fn header_u64(headers: &HeaderMap, name: &str) -> Option<u64> {
headers
.get(name)
.and_then(|v| v.to_str().ok())
.and_then(|s| s.trim().parse::<u64>().ok())
}
fn reset_delay(headers: &HeaderMap) -> Option<Duration> {
let reset = header_u64(headers, "x-ratelimit-reset")?;
let now = SystemTime::now().duration_since(UNIX_EPOCH).ok()?.as_secs();
Some(Duration::from_secs(reset.saturating_sub(now)))
}
#[derive(Default)]
struct BudgetState {
slept: Duration,
tripped: Option<(u16, Option<Duration>)>,
notices: Vec<String>,
}
pub struct FetchBudget {
sleep_budget: Duration,
state: Mutex<BudgetState>,
}
impl FetchBudget {
pub fn new() -> Self {
Self::with_sleep_budget(RATE_LIMIT_SLEEP_BUDGET)
}
pub fn with_sleep_budget(sleep_budget: Duration) -> Self {
Self {
sleep_budget,
state: Mutex::new(BudgetState::default()),
}
}
fn lock(&self) -> std::sync::MutexGuard<'_, BudgetState> {
self.state.lock().unwrap_or_else(|e| e.into_inner())
}
pub(crate) fn reserve(&self, delay: Duration, status: u16) -> Result<Duration, CollectError> {
let mut state = self.lock();
if let Some((status, retry_after)) = state.tripped {
return Err(CollectError::Throttled {
status,
retry_after,
});
}
if state.slept + delay > self.sleep_budget {
state.tripped = Some((status, Some(delay)));
return Err(CollectError::Throttled {
status,
retry_after: Some(delay),
});
}
state.slept += delay;
Ok(delay)
}
pub(crate) fn trip(&self, status: u16, retry_after: Option<Duration>) -> CollectError {
let mut state = self.lock();
if state.tripped.is_none() {
state.tripped = Some((status, retry_after));
}
CollectError::Throttled {
status,
retry_after,
}
}
pub(crate) fn tripped_error(&self) -> Option<CollectError> {
self.lock()
.tripped
.map(|(status, retry_after)| CollectError::Throttled {
status,
retry_after,
})
}
pub(crate) fn note_truncation(&self, message: impl Into<String>) {
self.lock().notices.push(message.into());
}
pub(crate) fn notices(&self) -> Vec<String> {
self.lock().notices.clone()
}
}
impl Default for FetchBudget {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
#[path = "budget_tests.rs"]
mod budget_tests;