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