1 /**
2   Utilities for tracking and managing rate limits.
3 */
4 module dscord.api.ratelimit;
5 
6 import std.conv,
7        std.math,
8        std.random,
9        core.time,
10        core.sync.mutex;
11 
12 import vibe.core.core;
13 
14 import dscord.api.routes,
15        dscord.util.time;
16 
17 /// Return a random backoff duration (by default between 0.5 and 3 seconds)
18 Duration randomBackoff(int low=500, int high=3000) {
19   int milliseconds = uniform(low, high);
20   return milliseconds.msecs;
21 }
22 
23 /**
24   Stores the rate limit state for a given bucket.
25 */
26 struct RateLimitState {
27   int remaining;
28 
29   /// Time at which this rate limit resets
30   long resetTime;
31 
32   /// Returns true if this request is valid
33   bool willRateLimit() {
34     if (this.remaining - 1 < 0) {
35       if (getUnixTime() <= this.resetTime) {
36         return true;
37       }
38     }
39 
40     return false;
41   }
42 
43   /// Return the time that needs to be waited before another request can be made
44   Duration waitTime() {
45     return (this.resetTime - getUnixTime()).seconds + 500.msecs;
46   }
47 }
48 
49 /**
50   RateLimiter provides an interface for rate limiting HTTP Requests.
51 */
52 class RateLimiter {
53   ManualEvent[Bucket]     cooldowns;
54   RateLimitState[Bucket]  states;
55 
56   /// Cooldown a bucket for a given duration. Blocks ALL requests from completing.
57   void cooldown(Bucket bucket, Duration duration) {
58     if (bucket in this.cooldowns) {
59       this.cooldowns[bucket].wait();
60     } else {
61       this.cooldowns[bucket] = createManualEvent();
62       sleep(duration);
63       this.cooldowns[bucket].emit();
64       this.cooldowns.remove(bucket);
65     }
66   }
67 
68   /**
69     Check whether a request can be made for a bucket. If the bucket is on cooldown,
70     wait until the cooldown resets before returning.
71   */
72   bool check(Bucket bucket, Duration timeout) {
73     // If we're currently waiting for a cooldown, join the waiting club
74     if (bucket in this.cooldowns) {
75       if (this.cooldowns[bucket].wait(timeout, 0) != 0) {
76         return false;
77       }
78     }
79 
80     // If we don't have the bucket cached, return
81     if (bucket !in this.states) return true;
82 
83     // If this request will rate limit, wait until it won't anymore
84     if (this.states[bucket].willRateLimit()) {
85       this.cooldown(bucket, this.states[bucket].waitTime());
86     }
87 
88     return true;
89   }
90 
91   /// Update a given bucket with headers returned from a request.
92   void update(Bucket bucket, string remaining, string reset, string retryAfter) {
93     long resetSeconds = (reset.to!long);
94 
95     // If we have a retryAfter header, it may be more accurate
96     if (retryAfter != "") {
97       FloatingPointControl fpctrl;
98       fpctrl.rounding = FloatingPointControl.roundUp;
99       long retryAfterSeconds = rndtol(retryAfter.to!long / 1000.0);
100 
101       long nextRequestAt = getUnixTime() + retryAfterSeconds;
102       if (nextRequestAt > resetSeconds) {
103         resetSeconds = nextRequestAt;
104       }
105     }
106 
107     // Create a new RateLimitState if one doesn't exist
108     if (bucket !in this.states) {
109       this.states[bucket] = RateLimitState();
110     }
111 
112     // Save our remaining requests and reset seconds
113     this.states[bucket].remaining = remaining.to!int;
114     this.states[bucket].resetTime = resetSeconds;
115   }
116 }
117