aboutsummaryrefslogtreecommitdiff
path: root/src/client/rest/ratelimiting.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/rest/ratelimiting.rs')
-rw-r--r--src/client/rest/ratelimiting.rs240
1 files changed, 240 insertions, 0 deletions
diff --git a/src/client/rest/ratelimiting.rs b/src/client/rest/ratelimiting.rs
new file mode 100644
index 0000000..65af1a3
--- /dev/null
+++ b/src/client/rest/ratelimiting.rs
@@ -0,0 +1,240 @@
+//! Routes are used for ratelimiting. These are to differentiate between the
+//! different _types_ of routes - such as getting the current user's channels -
+//! for the most part, with the exception being major parameters.
+//!
+//! [Taken from] the Discord docs, major parameters are:
+//!
+//! > Additionally, rate limits take into account major parameters in the URL.
+//! > For example, `/channels/:channel_id` and
+//! > `/channels/:channel_id/messages/:message_id` both take `channel_id` into
+//! > account when generating rate limits since it's the major parameter. The
+//! only current major parameters are `channel_id` and `guild_id`.
+//!
+//! This results in the two URIs of `GET /channels/4/messages/7` and
+//! `GET /channels/5/messages/8` being rate limited _separately_. However, the
+//! two URIs of `GET /channels/10/messages/11` and
+//! `GET /channels/10/messages/12` will count towards the "same ratelimit", as
+//! the major parameter - `10` is equivilant in both URIs' format.
+//!
+//! # Examples
+//!
+//! First: taking the first two URIs - `GET /channels/4/messages/7` and
+//! `GET /channels/5/messages/8` - and assuming both buckets have a `limit` of
+//! `10`, requesting the first URI will result in the response containing a
+//! `remaining` of `9`. Immediately after - prior to buckets resetting -
+//! performing a request to the _second_ URI will also contain a `remaining` of
+//! `9` in the response, as the major parameter - `channel_id` - is different
+//! in the two requests (`4` and `5`).
+//!
+//! Second: take for example the last two URIs. Assuming the bucket's `limit` is
+//! `10`, requesting the first URI will return a `remaining` of `9` in the
+//! response. Immediately after - prior to buckets resetting - performing a
+//! request to the _second_ URI will return a `remaining` of `8` in the
+//! response, as the major parameter - `channel_id` - is equivilant for the two
+//! requests (`10`).
+//!
+//!
+//! With the examples out of the way: major parameters are why some variants
+//! (i.e. all of the channel/guild variants) have an associated u64 as data.
+//! This is the Id of the parameter, differentiating between different
+//! ratelimits.
+
+use hyper::client::{RequestBuilder, Response};
+use hyper::header::Headers;
+use hyper::status::StatusCode;
+use std::collections::HashMap;
+use std::str;
+use std::sync::{Arc, Mutex};
+use std::thread;
+use std::time::Duration;
+use time;
+use ::internal::prelude::*;
+
+lazy_static! {
+ static ref GLOBAL: Arc<Mutex<RateLimit>> = Arc::new(Mutex::new(RateLimit::default()));
+ static ref ROUTES: Arc<Mutex<HashMap<Route, RateLimit>>> = Arc::new(Mutex::new(HashMap::default()));
+}
+
+#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
+pub enum Route {
+ ChannelsId(u64),
+ ChannelsIdInvites(u64),
+ ChannelsIdMessages(u64),
+ ChannelsIdMessagesBulkDelete(u64),
+ ChannelsIdMessagesId(u64),
+ ChannelsIdMessagesIdAck(u64),
+ ChannelsIdMessagesIdReactions(u64),
+ ChannelsIdMessagesIdReactionsUserIdType(u64),
+ ChannelsIdPermissionsOverwriteId(u64),
+ ChannelsIdPins(u64),
+ ChannelsIdPinsMessageId(u64),
+ ChannelsIdTyping(u64),
+ ChannelsIdWebhooks(u64),
+ Gateway,
+ GatewayBot,
+ Guilds,
+ GuildsId(u64),
+ GuildsIdBans(u64),
+ GuildsIdBansUserId(u64),
+ GuildsIdChannels(u64),
+ GuildsIdEmbed(u64),
+ GuildsIdEmojis(u64),
+ GuildsIdEmojisId(u64),
+ GuildsIdIntegrations(u64),
+ GuildsIdIntegrationsId(u64),
+ GuildsIdIntegrationsIdSync(u64),
+ GuildsIdInvites(u64),
+ GuildsIdMembersId(u64),
+ GuildsIdMembersIdRolesId(u64),
+ GuildsIdMembersMeNick(u64),
+ GuildsIdPrune(u64),
+ GuildsIdRegions(u64),
+ GuildsIdRoles(u64),
+ GuildsIdRolesId(u64),
+ GuildsIdWebhooks(u64),
+ InvitesCode,
+ UsersId,
+ UsersMe,
+ UsersMeChannels,
+ UsersMeConnections,
+ UsersMeGuilds,
+ UsersMeGuildsId,
+ VoiceRegions,
+ WebhooksId,
+ None,
+}
+
+pub fn perform<'a, F>(route: Route, f: F) -> Result<Response>
+ where F: Fn() -> RequestBuilder<'a> {
+
+ loop {
+ // Perform pre-checking here:
+ //
+ // - get the route's relevant rate
+ // - sleep if that route's already rate-limited until the end of the
+ // 'reset' time;
+ // - get the global rate;
+ // - sleep if there is 0 remaining
+ // - then, perform the request
+ {
+ let mut global = GLOBAL.lock().expect("global route lock poisoned");
+ global.pre_hook();
+ }
+
+ if route != Route::None {
+ if let Some(route) = ROUTES.lock().expect("routes poisoned").get_mut(&route) {
+ route.pre_hook();
+ }
+ }
+
+ let response = try!(super::retry(&f));
+
+ // Check if the request got ratelimited by checking for status 429,
+ // and if so, sleep for the value of the header 'retry-after' -
+ // which is in milliseconds - and then `continue` to try again
+ //
+ // If it didn't ratelimit, subtract one from the RateLimit's
+ // 'remaining'
+ //
+ // Update the 'reset' with the value of the 'x-ratelimit-reset'
+ // header
+ //
+ // It _may_ be possible for the limit to be raised at any time,
+ // so check if it did from the value of the 'x-ratelimit-limit'
+ // header. If the limit was 5 and is now 7, add 2 to the 'remaining'
+
+ if route != Route::None {
+ let redo = if response.headers.get_raw("x-ratelimit-global").is_some() {
+ let mut global = GLOBAL.lock().expect("global route lock poisoned");
+ global.post_hook(&response)
+ } else {
+ ROUTES.lock()
+ .expect("routes poisoned")
+ .entry(route)
+ .or_insert_with(RateLimit::default)
+ .post_hook(&response)
+ };
+
+ if redo.unwrap_or(false) {
+ continue;
+ }
+ }
+
+ return Ok(response);
+ }
+}
+
+#[derive(Clone, Debug, Default)]
+pub struct RateLimit {
+ limit: i64,
+ remaining: i64,
+ reset: i64,
+}
+
+impl RateLimit {
+ pub fn pre_hook(&mut self) {
+ if self.limit == 0 {
+ return;
+ }
+
+ let current_time = time::get_time().sec;
+
+ // The reset was in the past, so we're probably good.
+ if current_time > self.reset {
+ self.remaining = self.limit;
+
+ return;
+ }
+
+ let diff = (self.reset - current_time) as u64;
+
+ if self.remaining == 0 {
+ let delay = (diff * 1000) + 500;
+
+ debug!("Pre-emptive ratelimit for {:?}ms", delay);
+ thread::sleep(Duration::from_millis(delay));
+
+ return;
+ }
+
+ self.remaining -= 1;
+ }
+
+ pub fn post_hook(&mut self, response: &Response) -> Result<bool> {
+ if let Some(limit) = try!(get_header(&response.headers, "x-ratelimit-limit")) {
+ self.limit = limit;
+ }
+
+ if let Some(remaining) = try!(get_header(&response.headers, "x-ratelimit-remaining")) {
+ self.remaining = remaining;
+ }
+
+ if let Some(reset) = try!(get_header(&response.headers, "x-ratelimit-reset")) {
+ self.reset = reset;
+ }
+
+ Ok(if response.status != StatusCode::TooManyRequests {
+ false
+ } else if let Some(retry_after) = try!(get_header(&response.headers, "retry-after")) {
+ debug!("Ratelimited: {:?}ms", retry_after);
+ thread::sleep(Duration::from_millis(retry_after as u64));
+
+ true
+ } else {
+ false
+ })
+ }
+}
+
+fn get_header(headers: &Headers, header: &str) -> Result<Option<i64>> {
+ match headers.get_raw(header) {
+ Some(header) => match str::from_utf8(&header[0]) {
+ Ok(v) => match v.parse::<i64>() {
+ Ok(v) => Ok(Some(v)),
+ Err(_why) => Err(Error::Client(ClientError::RateLimitI64)),
+ },
+ Err(_why) => Err(Error::Client(ClientError::RateLimitUtf8)),
+ },
+ None => Ok(None),
+ }
+}