use memo_map::MemoMap;
use reqwest::{RequestBuilder, StatusCode};
#[cfg(feature = "tracing")]
use tracing::{self as log, Instrument};
use super::{RateLimit, RateLimitType};
use crate::time::{sleep, Duration};
use crate::{ResponseInfo, RiotApiConfig, RiotApiError, TryRequestError, TryRequestResult};
pub struct RegionalRequester {
app_rate_limit: RateLimit,
method_rate_limits: MemoMap<&'static str, RateLimit>,
}
impl RegionalRequester {
const NONE_STATUS_CODES: [StatusCode; 2] = [
StatusCode::NO_CONTENT, StatusCode::NOT_FOUND, ];
pub fn new() -> Self {
Self {
app_rate_limit: RateLimit::new(RateLimitType::Application),
method_rate_limits: MemoMap::new(),
}
}
pub async fn execute<'a>(
&'a self,
config: &'a RiotApiConfig,
method_id: &'static str,
request: RequestBuilder,
min_capacity: Option<f32>,
) -> TryRequestResult<ResponseInfo> {
let mut retries: u8 = 0;
let mut reqwest_errors = Vec::new();
loop {
let method_rate_limit = self
.method_rate_limits
.get_or_insert(&method_id, || RateLimit::new(RateLimitType::Method));
if let Some(min_capacity) = min_capacity {
if !RateLimit::acquire_both_if_above_capacity(
&self.app_rate_limit,
method_rate_limit,
min_capacity,
) {
return Err(TryRequestError::NotEnoughCapacity);
}
} else {
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;
}
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,
Err(e) => {
reqwest_errors.push(e);
if retries >= config.retries {
log::debug!(
"Request failed (retried {} times), failure, returning error.",
retries
);
break Err(TryRequestError::RiotApiError(RiotApiError::new(
reqwest_errors,
None,
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 {:?}.",
reqwest_errors.last().unwrap().to_string(), retries, delay,
);
let backoff = sleep(delay);
#[cfg(feature = "tracing")]
let backoff = backoff.instrument(tracing::info_span!("backoff"));
backoff.await;
retries += 1;
continue;
}
};
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);
let status = response.status();
let status_none = Self::NONE_STATUS_CODES.contains(&status);
if status.is_success() || status_none {
log::trace!(
"Response {} (retried {} times), success, returning result.",
status,
retries
);
break Ok(ResponseInfo {
response,
retries,
status_none,
reqwest_errors,
});
}
reqwest_errors.push(response.error_for_status_ref().err().unwrap_or_else(|| {
panic!(
"Unhandlable response status code, neither success nor failure: {}.",
status
)
}));
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(TryRequestError::RiotApiError(RiotApiError::new(
reqwest_errors,
None,
retries,
Some(response),
Some(status),
)));
}
match retry_after {
Some(delay) => {
log::debug!(
"Response {} (retried {} times), `retry-after` set, retrying after {:?}.",
status,
retries,
delay
);
}
None => {
let delay = Duration::from_secs(2 + 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;
}
}
retries += 1;
}
}
}
#[cfg(test)]
mod tests {
}