Skip to content

Commit

Permalink
Take 'Date' header into account when ratelimiting
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Zeyla Hellyer committed Apr 27, 2018
1 parent fb92b87 commit 40db3c0
Showing 1 changed file with 61 additions and 2 deletions.
63 changes: 61 additions & 2 deletions src/http/ratelimiting.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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])
Expand Down

0 comments on commit 40db3c0

Please sign in to comment.