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