1
0
Fork 1
mirror of https://github.com/MingweiSamuel/Riven.git synced 2025-03-23 15:13:15 -07:00
Riven/riven/src/req/regional_requester.rs
Jeffrey Dallatezza 181403c578
feat: retry networking errors ()
Co-authored-by: Jeffrey Dallatezza <jeffreydallatezza@Jeffreys-Laptop.local>
2024-03-06 12:32:07 -08:00

153 lines
6.4 KiB
Rust

use std::future::Future;
use std::sync::Arc;
use reqwest::{RequestBuilder, StatusCode};
#[cfg(feature = "tracing")]
use tracing::{self as log, Instrument};
use super::{RateLimit, RateLimitType};
use crate::time::{sleep, Duration};
use crate::util::InsertOnlyCHashMap;
use crate::{ResponseInfo, Result, RiotApiConfig, RiotApiError};
pub struct RegionalRequester {
/// The app rate limit.
app_rate_limit: RateLimit,
/// Method rate limits.
method_rate_limits: InsertOnlyCHashMap<&'static str, RateLimit>,
}
impl RegionalRequester {
/// HTTP status codes which are considered a success but will results in `None`.
const NONE_STATUS_CODES: [StatusCode; 2] = [
StatusCode::NO_CONTENT, // 204
StatusCode::NOT_FOUND, // 404
];
pub fn new() -> Self {
Self {
app_rate_limit: RateLimit::new(RateLimitType::Application),
method_rate_limits: InsertOnlyCHashMap::new(),
}
}
pub fn execute<'a>(
self: Arc<Self>,
config: &'a RiotApiConfig,
method_id: &'static str,
request: RequestBuilder,
) -> impl Future<Output = Result<ResponseInfo>> + 'a {
async move {
let mut retries: u8 = 0;
loop {
let method_rate_limit: Arc<RateLimit> = self
.method_rate_limits
.get_or_insert_with(method_id, || RateLimit::new(RateLimitType::Method));
// Rate limit.
let rate_limit = RateLimit::acquire_both(&self.app_rate_limit, &method_rate_limit);
#[cfg(feature = "tracing")]
let rate_limit = rate_limit.instrument(tracing::info_span!("rate_limit"));
rate_limit.await;
// Send request.
let request_clone = request
.try_clone()
.expect("Failed to clone request.")
.send();
#[cfg(feature = "tracing")]
let request_clone = request_clone.instrument(tracing::info_span!("request"));
let response = request_clone.await;
let response = match response {
Ok(response) => response,
// Check for lower level errors, like connection errors.
Err(e) => {
if retries >= config.retries {
log::debug!(
"Request failed (retried {} times), failure, returning error.",
retries
);
break Err(RiotApiError::new(e, retries, None, None));
}
let delay = Duration::from_secs(2_u64.pow(retries as u32));
log::debug!("Request failed with cause \"{}\", (retried {} times), using exponential backoff, retrying after {:?}.", e.to_string(), retries, delay);
let backoff = sleep(delay);
#[cfg(feature = "tracing")]
let backoff = backoff.instrument(tracing::info_span!("backoff"));
backoff.await;
retries += 1;
continue;
}
};
// Maybe update rate limits (based on response headers).
// Use single bar for no short circuiting.
let retry_after_app = self.app_rate_limit.on_response(config, &response);
let retry_after_method = method_rate_limit.on_response(config, &response);
let retry_after = retry_after_app.or(retry_after_method); // Note: Edge case if both are Some(_) not handled.
let status = response.status();
// Handle normal success / failure cases.
let status_none = Self::NONE_STATUS_CODES.contains(&status);
// Success case.
if status.is_success() || status_none {
log::trace!(
"Response {} (retried {} times), success, returning result.",
status,
retries
);
break Ok(ResponseInfo {
response,
retries,
status_none,
});
}
let err = response.error_for_status_ref().err().unwrap_or_else(|| {
panic!(
"Unhandlable response status code, neither success nor failure: {}.",
status
)
});
// Failure, may or may not be retryable.
// Not-retryable: no more retries or 4xx or ? (3xx, redirects exceeded).
// Retryable: retries remaining, and 429 or 5xx.
if retries >= config.retries
|| (StatusCode::TOO_MANY_REQUESTS != status && !status.is_server_error())
{
log::debug!(
"Response {} (retried {} times), failure, returning error.",
status,
retries
);
break Err(RiotApiError::new(
err,
retries,
Some(response),
Some(status),
));
}
// Is retryable, do exponential backoff if retry-after wasn't specified.
// 1 sec, 2 sec, 4 sec, 8 sec.
match retry_after {
None => {
let delay = Duration::from_secs(2_u64.pow(retries as u32));
log::debug!("Response {} (retried {} times), NO `retry-after`, using exponential backoff, retrying after {:?}.", status, retries, delay);
let backoff = sleep(delay);
#[cfg(feature = "tracing")]
let backoff = backoff.instrument(tracing::info_span!("backoff"));
backoff.await;
}
Some(delay) => {
log::debug!("Response {} (retried {} times), `retry-after` set, retrying after {:?}.", status, retries, delay);
}
}
retries += 1;
}
}
}
}
#[cfg(test)]
mod tests {
// use super::*;
}