1 module dscord.api.ratelimit;
2 
3 import std.conv,
4        std.math,
5        std.random,
6        core.time,
7        core.sync.mutex;
8 import vibe.core.core;
9 import discord.api.routes,
10        discord.util.time;
11 
12 Duration randomBackoff(int low=500, int high=3000) {
13   int milliseconds = uniform(low, high);
14   return milliseconds.msecs;
15 }
16 
17 /**
18   Stores the rate limit state for a given bucket.
19 */
20 struct RateLimitState {
21   int remaining;
22   long resetTime;
23   bool willRateLimit() {
24     if (this.remaining - 1 < 0) {
25       if (getUnixTime() <= this.resetTime) {
26         return true;
27       }
28     }
29     return false;
30   }
31   Duration waitTime() {
32     return (this.resetTime - getUnixTime()).seconds + 500.msecs;
33   }
34 }
35 
36 /**
37   RateLimiter provides an interface for rate limiting HTTP requests.
38 */
39 class RateLimiter {
40   ManualEvent[Bucket]     cooldowns;
41   RateLimitState[Bucket]  states;
42 
43   // Cooldown a bucket for a given duration. Blocks ALL requests from completing.
44   void cooldown(Bucket bucket, Duration duration) {
45     if (bucket in this.cooldowns) {
46       this.cooldowns[bucket].wait();
47     } else {
48       this.cooldowns[bucket] = createManualEvent();
49       sleep(duration);
50       this.cooldowns[bucket].emit();
51       this.cooldowns.remove(bucket);
52     }
53   }
54 
55   /**
56     Check whether a request can be made for a bucket. If the bucket is on cooldown,
57     wait until the cooldown resets before returning.
58   */
59   bool check(Bucket bucket, Duration timeout) {
60     if (bucket in this.cooldowns) {
61       if (this.cooldowns[bucket].wait(timeout, 0) != 0) {
62         return false;
63       }
64     }
65 
66     if (bucket !in this.states) {
67         return true;
68     }
69     if (this.states[bucket].willRateLimit()) {
70       this.cooldown(bucket, this.states[bucket].waitTime());
71     }
72     return true;
73   }
74 
75   /// Update a given bucket with headers returned from a request.
76   void update(Bucket bucket, string remaining, string reset, string retryAfter) {
77     long resetSeconds = (reset.to!long);
78 
79     if (retryAfter != "") {
80       FloatingPointControl fpctrl;
81       fpctrl.rounding = FloatingPointControl.roundUp;
82       long retryAfterSeconds = rndtol(retryAfter.to!long / 1000.0);
83       long nextRequestAt = getUnixTime() + retryAfterSeconds;
84       if (nextRequestAt > resetSeconds) {
85         resetSeconds = nextRequestAt;
86       }
87     }
88 
89     // Create a new RateLimitState if there is none
90     if (bucket !in this.states) {
91       this.states[bucket] = RateLimitState();
92     }
93 
94     // Save our remaining requests and reset seconds
95     this.states[bucket].remaining = remaining.to!int;
96     this.states[bucket].resetTime = resetSeconds;
97   }
98 }