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 }