use crate::Error;
use serde::{Deserialize, Serialize};
use std::time::Duration;
const RETRY_DELAY: Duration = Duration::from_millis(250);
const MAX_RETRY_AFTER: Duration = Duration::from_secs(5);
pub fn retry_delay(status: reqwest::StatusCode, retry_after: Option<&str>) -> Option<Duration> {
match status {
reqwest::StatusCode::TOO_MANY_REQUESTS => Some(
retry_after
.and_then(|value| value.parse::<u64>().ok())
.map(Duration::from_secs)
.unwrap_or(RETRY_DELAY)
.min(MAX_RETRY_AFTER),
),
reqwest::StatusCode::BAD_GATEWAY
| reqwest::StatusCode::SERVICE_UNAVAILABLE
| reqwest::StatusCode::GATEWAY_TIMEOUT => Some(RETRY_DELAY),
_ => None,
}
}
pub async fn send_with_retry(
request: reqwest::RequestBuilder,
) -> reqwest::Result<reqwest::Response> {
let retry = request.try_clone();
match request.send().await {
Ok(response) => {
let delay = retry_delay(
response.status(),
response
.headers()
.get(reqwest::header::RETRY_AFTER)
.and_then(|value| value.to_str().ok()),
);
if let (Some(request), Some(delay)) = (retry, delay) {
tokio::time::sleep(delay).await;
request.send().await
} else {
Ok(response)
}
}
Err(error) if error.is_connect() || error.is_timeout() => {
if let Some(request) = retry {
tokio::time::sleep(RETRY_DELAY).await;
request.send().await
} else {
Err(error)
}
}
Err(error) => Err(error),
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ProviderOutcome {
pub provider: String,
pub status: ProviderStatus,
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ProviderStatus {
Success,
Unavailable,
RateLimited,
Failed,
}
impl ProviderOutcome {
pub fn success(provider: impl Into<String>) -> Self {
Self {
provider: provider.into(),
status: ProviderStatus::Success,
message: None,
}
}
pub fn from_error(provider: impl Into<String>, error: &Error) -> Self {
let status = match error {
Error::Reqwest(error)
if error.status() == Some(reqwest::StatusCode::TOO_MANY_REQUESTS) =>
{
ProviderStatus::RateLimited
}
Error::Reqwest(error) if error.is_timeout() || error.is_connect() => {
ProviderStatus::Unavailable
}
_ => ProviderStatus::Failed,
};
Self {
provider: provider.into(),
status,
message: Some(error.to_string()),
}
}
pub fn succeeded(&self) -> bool {
self.status == ProviderStatus::Success
}
pub fn aggregate(provider: impl Into<String>, outcomes: &[Self]) -> Self {
let status = outcomes
.iter()
.map(|outcome| outcome.status)
.max_by_key(|status| match status {
ProviderStatus::Success => 0,
ProviderStatus::Unavailable => 1,
ProviderStatus::RateLimited => 2,
ProviderStatus::Failed => 3,
})
.unwrap_or(ProviderStatus::Success);
let messages: Vec<_> = outcomes
.iter()
.filter_map(|outcome| outcome.message.as_deref())
.collect();
Self {
provider: provider.into(),
status,
message: (!messages.is_empty()).then(|| messages.join("; ")),
}
}
}
pub fn default_success() -> ProviderOutcome {
ProviderOutcome::success("legacy")
}
impl std::fmt::Display for ProviderStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::Success => "success",
Self::Unavailable => "unavailable",
Self::RateLimited => "rate_limited",
Self::Failed => "failed",
})
}
}