diff options
| author | Zeyla Hellyer <[email protected]> | 2018-04-27 09:55:21 -0700 |
|---|---|---|
| committer | Zeyla Hellyer <[email protected]> | 2018-04-27 09:55:21 -0700 |
| commit | 40db3c024dd5e13c5a02e10ab4f7a7e9c31a6876 (patch) | |
| tree | 75ccb2333565a341821aaaba1914922f5ed61e93 /src/http | |
| parent | Remove a usage of Clone::clone (diff) | |
| download | serenity-40db3c024dd5e13c5a02e10ab4f7a7e9c31a6876.tar.xz serenity-40db3c024dd5e13c5a02e10ab4f7a7e9c31a6876.zip | |
Take 'Date' header into account when ratelimiting
Take the 'Date' header into account when ratelimiting so that a
server-client time offset can be calculated. This improves ratelimiting
precision on the seconds-level, but is still prone to bad millisecond
precision offsets.
Diffstat (limited to 'src/http')
| -rw-r--r-- | src/http/ratelimiting.rs | 63 |
1 files changed, 61 insertions, 2 deletions
diff --git a/src/http/ratelimiting.rs b/src/http/ratelimiting.rs index bb59d21..ad1ea8f 100644 --- a/src/http/ratelimiting.rs +++ b/src/http/ratelimiting.rs @@ -40,7 +40,7 @@ //! [Taken from]: https://discordapp.com/developers/docs/topics/rate-limits#rate-limits #![allow(zero_ptr)] -use chrono::Utc; +use chrono::{DateTime, Utc}; use hyper::client::{RequestBuilder, Response}; use hyper::header::Headers; use hyper::status::StatusCode; @@ -56,6 +56,22 @@ use std::{ }; use super::{HttpError, LightMethod}; +/// The calculated offset of the time difference between Discord and the client +/// in seconds. +/// +/// This does not have millisecond precision as calculating that isn't +/// realistic. +/// +/// This is used in ratelimiting to help determine how long to wait for +/// pre-emptive ratelimits. For example, if the client is 2 seconds ahead, then +/// the client would think the ratelimit is over 2 seconds before it actually is +/// and would then send off queued requests. Using an offset, we can know that +/// there's actually still 2 seconds left (+/- some milliseconds). +/// +/// This isn't a definitive solution to fix all problems, but it can help with +/// some precision gains. +static mut OFFSET: Option<i64> = None; + lazy_static! { /// The global mutex is a mutex unlocked and then immediately re-locked /// prior to every request, to abide by Discord's global ratelimit. @@ -387,6 +403,17 @@ pub(crate) fn perform<'a, F>(route: Route, f: F) -> Result<Response> let response = super::retry(&f)?; + // Check if an offset has been calculated yet to determine the time + // difference from Discord can the client. + // + // Refer to the documentation for `OFFSET` for more information. + // + // This should probably only be a one-time check, although we may want + // to choose to check this often in the future. + if unsafe { OFFSET }.is_none() { + calculate_offset(response.headers.get_raw("date")); + } + // 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 @@ -458,7 +485,9 @@ impl RateLimit { return; } - let current_time = Utc::now().timestamp(); + let offset = unsafe { OFFSET }.unwrap_or(0); + let now = Utc::now().timestamp(); + let current_time = now - offset; // The reset was in the past, so we're probably good. if current_time > self.reset { @@ -511,6 +540,36 @@ impl RateLimit { } } +fn calculate_offset(header: Option<&[Vec<u8>]>) { + // Get the current time as soon as possible. + let now = Utc::now().timestamp(); + + // First get the `Date` header's value and parse it as UTF8. + let header = header + .and_then(|h| h.get(0)) + .and_then(|x| str::from_utf8(x).ok()); + + if let Some(date) = header { + // Replace the `GMT` timezone with an offset, and then parse it + // into a chrono DateTime. If it parses correctly, calculate the + // diff and then set it as the offset. + let s = date.replace("GMT", "+0000"); + let parsed = DateTime::parse_from_str(&s, "%a, %d %b %Y %T %z"); + + if let Ok(parsed) = parsed { + let offset = parsed.timestamp(); + + let diff = offset - now; + + unsafe { + OFFSET = Some(diff); + + debug!("[ratelimiting] Set the ratelimit offset to {}", diff); + } + } + } +} + fn parse_header(headers: &Headers, header: &str) -> Result<Option<i64>> { headers.get_raw(header).map_or(Ok(None), |header| { str::from_utf8(&header[0]) |