use std::collections::HashMap;
use super::*;
#[derive(Default)]
pub(super) struct Retries {
items: Vec<(tokio::time::Instant, Url, usize, u32, Option<ApiError>)>,
immediate: bool,
pending: HashMap<String, String>,
outcomes: HashMap<String, pagesift_stat::beta::BetaCounts>,
}
impl Retries {
pub(super) fn new(backoff: bool) -> Self {
Self {
immediate: !backoff,
..Self::default()
}
}
fn worth_retrying(&self, class: &str) -> bool {
let delta = pagesift_stat::Delta::DEFAULT;
self.outcomes.get(class).is_none_or(|c| {
pagesift_stat::beta::wilson(c.success, c.trials(), delta).1 >= delta.get()
})
}
pub(super) fn offer(
&mut self,
result: &Result<pagesift_api::types::Document, ApiError>,
url: &Url,
depth: usize,
attempt: u32,
seed: bool,
) -> bool {
let (max, base) = if seed {
(SEED_MAX_RETRIES, SEED_RETRY_BASE)
} else {
(MAX_RETRIES, RETRY_BASE)
};
let wait = retry_wait(result, attempt, base)
.map(|w| if self.immediate { Duration::ZERO } else { w });
if let Some(prev) = self.pending.remove(url.as_str()) {
self.outcomes
.entry(prev)
.or_default()
.observe(wait.is_none());
}
let class = failure_class(result);
match wait {
Some(wait)
if attempt < max
&& wait <= MAX_WAIT
&& class.as_deref().is_none_or(|c| self.worth_retrying(c)) =>
{
if let Some(c) = class {
self.pending.insert(url.to_string(), c);
}
self.items.push((
tokio::time::Instant::now() + wait,
url.clone(),
depth,
attempt + 1,
result.as_ref().err().cloned(),
));
true
}
_ => false,
}
}
pub(super) fn due(&mut self) -> Option<(Url, usize, u32)> {
let now = tokio::time::Instant::now();
let i = (0..self.items.len())
.filter(|&i| self.items[i].0 <= now)
.min_by_key(|&i| self.items[i].0)?;
let (_, u, d, a, _) = self.items.swap_remove(i);
Some((u, d, a))
}
pub(super) fn drop_host(&mut self, host: &str) -> Vec<(Url, Option<ApiError>)> {
let mut out = Vec::new();
self.items.retain(|(_, u, _, _, e)| {
let keep = u.host_str() != Some(host);
if !keep {
out.push((u.clone(), e.clone()));
}
keep
});
for (u, _) in &out {
self.pending.remove(u.as_str());
}
out
}
pub(super) fn is_empty(&self) -> bool {
self.items.is_empty()
}
pub(super) fn next_due(&self) -> Option<tokio::time::Instant> {
self.items.iter().map(|r| r.0).min()
}
}
fn failure_class(result: &Result<pagesift_api::types::Document, ApiError>) -> Option<String> {
match result {
Ok(doc) if doc.metadata.status_code >= 400 => Some(doc.metadata.status_code.to_string()),
Ok(_) => None,
Err(e) => Some(format!("{:?}", e.code)),
}
}
pub(super) fn retry_wait(
result: &Result<pagesift_api::types::Document, ApiError>,
attempt: u32,
base: Duration,
) -> Option<Duration> {
let backoff = base * 2u32.pow(attempt.min(6));
match result {
Ok(doc) => matches!(
doc.metadata.status_code,
408 | 429 | 500 | 502 | 503 | 504 | 522 | 524
)
.then_some(backoff),
Err(e) => match e.code {
ErrorCode::HostCooldown | ErrorCode::RateLimited => {
Some(e.retry_after.map_or(backoff, |s| {
Duration::from_secs(s).max(Duration::from_millis(100))
}))
}
ErrorCode::Timeout
| ErrorCode::NetworkError
| ErrorCode::UpstreamError
| ErrorCode::Busy => Some(backoff),
_ => None,
},
}
}