riven 2.78.0

Riot Games API Library
Documentation
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 {
    /// The app rate limit.
    app_rate_limit: RateLimit,
    /// Method rate limits.
    method_rate_limits: MemoMap<&'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: 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 {
                // Never sleep, return `None` if we don't have enough capacity.
                if !RateLimit::acquire_both_if_above_capacity(
                    &self.app_rate_limit,
                    method_rate_limit,
                    min_capacity,
                ) {
                    return Err(TryRequestError::NotEnoughCapacity);
                }
            } else {
                // Sleep until we can aquire both.
                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) => {
                    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;
                }
            };
            // 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,
                    reqwest_errors,
                });
            }
            reqwest_errors.push(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(TryRequestError::RiotApiError(RiotApiError::new(
                    reqwest_errors,
                    None,
                    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 {
                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 {
    // use super::*;
}