Class: DiscordRDA::RateLimiter
- Inherits:
-
Object
- Object
- DiscordRDA::RateLimiter
- Defined in:
- lib/discord_rda/connection/rate_limiter.rb
Overview
Production-ready Rate Limiter for Discord REST API. Implements precise token bucket algorithm with async timer-based resets.
Defined Under Namespace
Classes: RateLimitInfo
Instance Attribute Summary collapse
-
#global_reset_at ⇒ Float
readonly
Global rate limit reset timestamp.
-
#limits ⇒ Hash<String, RateLimitInfo>
readonly
Rate limit info per route.
-
#logger ⇒ Logger
readonly
Logger instance.
-
#waiters ⇒ Hash<String, Array<Async::Condition>]
readonly
] Waiters per route.
Instance Method Summary collapse
-
#acquire(route) ⇒ void
Acquire permission to make a request with precise timing.
-
#bucket_id(route) ⇒ String?
Get bucket ID for a route.
-
#clear ⇒ void
Clear all rate limits.
-
#info(route) ⇒ RateLimitInfo?
Get rate limit info for a route.
-
#initialize(logger: nil) ⇒ RateLimiter
constructor
Initialize rate limiter.
-
#limited?(route) ⇒ Boolean
Check if route is rate limited.
-
#reset_time(route) ⇒ Time?
Get reset time for a route.
-
#status ⇒ Hash
Get comprehensive status.
-
#time_until_reset(route) ⇒ Float?
Get time until reset for a route.
-
#update(route, response) ⇒ void
Update rate limit info from response headers with precise timing.
-
#wait_for_route(route) ⇒ void
Wait for a route to be available (async-friendly).
Constructor Details
#initialize(logger: nil) ⇒ RateLimiter
Initialize rate limiter
28 29 30 31 32 33 34 35 36 |
# File 'lib/discord_rda/connection/rate_limiter.rb', line 28 def initialize(logger: nil) @logger = logger @limits = {} @waiters = Hash.new { |h, k| h[k] = [] } @mutex = Mutex.new @global_reset_at = nil @timers = {} @semaphore = Async::Semaphore.new(1) end |
Instance Attribute Details
#global_reset_at ⇒ Float (readonly)
Returns Global rate limit reset timestamp.
24 25 26 |
# File 'lib/discord_rda/connection/rate_limiter.rb', line 24 def global_reset_at @global_reset_at end |
#limits ⇒ Hash<String, RateLimitInfo> (readonly)
Returns Rate limit info per route.
15 16 17 |
# File 'lib/discord_rda/connection/rate_limiter.rb', line 15 def limits @limits end |
#logger ⇒ Logger (readonly)
Returns Logger instance.
21 22 23 |
# File 'lib/discord_rda/connection/rate_limiter.rb', line 21 def logger @logger end |
#waiters ⇒ Hash<String, Array<Async::Condition>] (readonly)
Returns ] Waiters per route.
18 19 20 |
# File 'lib/discord_rda/connection/rate_limiter.rb', line 18 def waiters @waiters end |
Instance Method Details
#acquire(route) ⇒ void
This method returns an undefined value.
Acquire permission to make a request with precise timing
41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
# File 'lib/discord_rda/connection/rate_limiter.rb', line 41 def acquire(route) # Check global rate limit first wait_for_global if @global_reset_at # Check route-specific limit with precise timing info = @mutex.synchronize { @limits[route] } return unless info if info.remaining <= 0 && info.reset_after > 0 now = Time.now.to_f wait_time = [info.reset_after - (now - info.last_updated.to_f), 0].max if wait_time > 0 @logger&.info('Rate limited, waiting', route: route, seconds: wait_time.round(3), bucket: info.bucket) precise_sleep(wait_time) end end end |
#bucket_id(route) ⇒ String?
Get bucket ID for a route
154 155 156 |
# File 'lib/discord_rda/connection/rate_limiter.rb', line 154 def bucket_id(route) @mutex.synchronize { @limits[route]&.bucket } end |
#clear ⇒ void
This method returns an undefined value.
Clear all rate limits
182 183 184 185 186 187 188 189 |
# File 'lib/discord_rda/connection/rate_limiter.rb', line 182 def clear @mutex.synchronize do @limits.clear @timers.each_value(&:stop) if @timers @timers.clear @global_reset_at = nil end end |
#info(route) ⇒ RateLimitInfo?
Get rate limit info for a route
110 111 112 |
# File 'lib/discord_rda/connection/rate_limiter.rb', line 110 def info(route) @mutex.synchronize { @limits[route] } end |
#limited?(route) ⇒ Boolean
Check if route is rate limited
117 118 119 120 121 122 123 124 125 126 127 128 129 130 |
# File 'lib/discord_rda/connection/rate_limiter.rb', line 117 def limited?(route) info = @mutex.synchronize { @limits[route] } return false unless info if info.remaining > 0 false elsif info.reset_after <= 0 false else now = Time.now.to_f elapsed = now - info.last_updated.to_f elapsed < info.reset_after end end |
#reset_time(route) ⇒ Time?
Get reset time for a route
147 148 149 |
# File 'lib/discord_rda/connection/rate_limiter.rb', line 147 def reset_time(route) @mutex.synchronize { @limits[route]&.reset } end |
#status ⇒ Hash
Get comprehensive status
193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 |
# File 'lib/discord_rda/connection/rate_limiter.rb', line 193 def status @mutex.synchronize do { global_limited: @global_reset_at && Time.now.to_f < @global_reset_at, global_reset_in: @global_reset_at ? [@global_reset_at - Time.now.to_f, 0].max : nil, routes_tracked: @limits.size, routes: @limits.transform_values do |info| { limit: info.limit, remaining: info.remaining, reset_after: info.reset_after, bucket: info.bucket, limited: limited?(@limits.key(info)) } end } end end |
#time_until_reset(route) ⇒ Float?
Get time until reset for a route
135 136 137 138 139 140 141 142 |
# File 'lib/discord_rda/connection/rate_limiter.rb', line 135 def time_until_reset(route) info = @mutex.synchronize { @limits[route] } return nil unless info return nil if info.remaining > 0 elapsed = Time.now.to_f - info.last_updated.to_f [info.reset_after - elapsed, 0].max end |
#update(route, response) ⇒ void
This method returns an undefined value.
Update rate limit info from response headers with precise timing
64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 |
# File 'lib/discord_rda/connection/rate_limiter.rb', line 64 def update(route, response) headers = response.headers # Check for global rate limit global = headers['x-ratelimit-global'] if global == 'true' retry_after = headers['retry-after']&.to_f || 1.0 @global_reset_at = Time.now.to_f + retry_after @logger&.error('Global rate limit hit', reset_in: retry_after) schedule_global_reset(retry_after) notify_waiters(route) return end # Parse rate limit headers limit = headers['x-ratelimit-limit']&.to_i remaining = headers['x-ratelimit-remaining']&.to_i reset = headers['x-ratelimit-reset']&.to_f reset_after = headers['x-ratelimit-reset-after']&.to_f bucket = headers['x-ratelimit-bucket'] return unless limit now = Time.now info = RateLimitInfo.new( limit: limit, remaining: remaining || 0, reset: reset ? Time.at(reset) : nil, reset_after: reset_after || 0, bucket: bucket, last_updated: now ) @mutex.synchronize { @limits[route] = info } # Schedule precise reset timer if depleted if remaining && remaining <= 0 && reset_after && reset_after > 0 schedule_route_reset(route, reset_after) end @logger&.debug('Rate limit updated', route: route, bucket: bucket, remaining: remaining, reset_after: reset_after&.round(3)) end |
#wait_for_route(route) ⇒ void
This method returns an undefined value.
Wait for a route to be available (async-friendly)
161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 |
# File 'lib/discord_rda/connection/rate_limiter.rb', line 161 def wait_for_route(route) return unless limited?(route) wait_time = time_until_reset(route) return unless wait_time && wait_time > 0 condition = Async::Condition.new @mutex.synchronize { @waiters[route] << condition } @logger&.debug('Waiting for route', route: route, seconds: wait_time.round(3)) Async do |task| task.sleep(wait_time) condition.signal end condition.wait end |