Class: DiscordRDA::InvalidRequestBucket

Inherits:
Object
  • Object
show all
Defined in:
lib/discord_rda/connection/invalid_bucket.rb

Overview

Production-ready Invalid Request Bucket - Prevents 1-hour Discord bans. Implements global request pausing when approaching invalid request limits.

Constant Summary collapse

DEFAULT_LIMIT =

Default values per Discord’s documentation

10_000
DEFAULT_INTERVAL =

10 minutes in milliseconds

10 * 60 * 1000
WARNING_THRESHOLD =

Warn when remaining drops below this

100
PAUSE_THRESHOLD =

Pause all requests when remaining drops below this

50

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(limit: DEFAULT_LIMIT, interval: DEFAULT_INTERVAL, logger: nil) ⇒ InvalidRequestBucket

Initialize invalid request bucket

Parameters:

  • limit (Integer) (defaults to: DEFAULT_LIMIT)

    Maximum invalid requests (default: 10000)

  • interval (Integer) (defaults to: DEFAULT_INTERVAL)

    Time window in milliseconds (default: 600000)

  • logger (Logger) (defaults to: nil)

    Logger instance



33
34
35
36
37
38
39
40
41
42
43
# File 'lib/discord_rda/connection/invalid_bucket.rb', line 33

def initialize(limit: DEFAULT_LIMIT, interval: DEFAULT_INTERVAL, logger: nil)
  @limit = limit
  @interval = interval
  @remaining = limit
  @logger = logger
  @mutex = Mutex.new
  @frozen_at = nil
  @reset_timer = nil
  @globally_paused = false
  @pause_condition = Async::Condition.new
end

Instance Attribute Details

#globally_pausedBoolean (readonly)

Returns Whether globally paused due to approaching limit.

Returns:

  • (Boolean)

    Whether globally paused due to approaching limit



27
28
29
# File 'lib/discord_rda/connection/invalid_bucket.rb', line 27

def globally_paused
  @globally_paused
end

#intervalInteger (readonly)

Returns Time window in milliseconds.

Returns:

  • (Integer)

    Time window in milliseconds



18
19
20
# File 'lib/discord_rda/connection/invalid_bucket.rb', line 18

def interval
  @interval
end

#limitInteger (readonly)

Returns Maximum invalid requests allowed.

Returns:

  • (Integer)

    Maximum invalid requests allowed



15
16
17
# File 'lib/discord_rda/connection/invalid_bucket.rb', line 15

def limit
  @limit
end

#loggerLogger (readonly)

Returns Logger instance.

Returns:

  • (Logger)

    Logger instance



24
25
26
# File 'lib/discord_rda/connection/invalid_bucket.rb', line 24

def logger
  @logger
end

#remainingInteger (readonly)

Returns Current remaining requests.

Returns:

  • (Integer)

    Current remaining requests



21
22
23
# File 'lib/discord_rda/connection/invalid_bucket.rb', line 21

def remaining
  @remaining
end

Instance Method Details

#handle_request(status) ⇒ void

This method returns an undefined value.

Handle a completed request response

Parameters:

  • status (Integer)

    HTTP status code



94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# File 'lib/discord_rda/connection/invalid_bucket.rb', line 94

def handle_request(status)
  # Only count 401, 403, 429, and 502 as invalid requests
  return unless invalid_status?(status)

  @mutex.synchronize do
    @frozen_at ||= Time.now.to_f * 1000
    @remaining -= 1

    @logger&.debug('Invalid request counted', status: status, remaining: @remaining, limit: @limit)

    # Schedule automatic reset
    schedule_reset unless @reset_timer

    # Check thresholds
    if @remaining == WARNING_THRESHOLD
      @logger&.warn('Approaching invalid request limit!', remaining: @remaining)
    elsif @remaining == PAUSE_THRESHOLD
      @globally_paused = true
      @logger&.error('CRITICAL: Pausing all requests to prevent 1-hour Discord ban!', remaining: @remaining)
    elsif @remaining <= 0
      @logger&.error('INVALID REQUEST LIMIT REACHED! All requests blocked for 10 minutes.')
    end
  end
end

#health_percentageFloat

Get percentage of remaining requests

Returns:

  • (Float)

    Percentage (0-100)



185
186
187
# File 'lib/discord_rda/connection/invalid_bucket.rb', line 185

def health_percentage
  @mutex.synchronize { (@remaining.to_f / @limit) * 100 }
end

#healthy?Boolean

Check if bucket is healthy

Returns:

  • (Boolean)

    True if well above warning threshold



179
180
181
# File 'lib/discord_rda/connection/invalid_bucket.rb', line 179

def healthy?
  @mutex.synchronize { @remaining > WARNING_THRESHOLD }
end

#release_pausevoid

This method returns an undefined value.

Release global pause (call after interval or manual intervention)



121
122
123
124
125
126
127
128
# File 'lib/discord_rda/connection/invalid_bucket.rb', line 121

def release_pause
  @mutex.synchronize do
    was_paused = @globally_paused
    @globally_paused = false
    @logger&.info('Global pause released. Resuming normal request processing.') if was_paused
    @pause_condition.signal
  end
end

#request_allowed?Boolean

Check if a request is allowed

Returns:

  • (Boolean)

    True if request can be made



80
81
82
83
84
85
86
87
88
89
# File 'lib/discord_rda/connection/invalid_bucket.rb', line 80

def request_allowed?
  @mutex.synchronize do
    return false if @globally_paused
    return true if @remaining > 0
    return true unless @frozen_at

    now = Time.now.to_f * 1000
    now >= (@frozen_at + @interval)
  end
end

#resetvoid

This method returns an undefined value.

Reset the bucket (after interval has passed)



132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
# File 'lib/discord_rda/connection/invalid_bucket.rb', line 132

def reset
  @mutex.synchronize do
    old_remaining = @remaining
    @remaining = @limit
    @frozen_at = nil
    @reset_timer = nil
    was_paused = @globally_paused
    @globally_paused = false

    if old_remaining < WARNING_THRESHOLD
      @logger&.info('Invalid request bucket reset', previous_remaining: old_remaining)
    end

    @pause_condition.signal if was_paused
  end
end

#statusHash

Get current status with detailed information

Returns:

  • (Hash)

    Bucket status



151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
# File 'lib/discord_rda/connection/invalid_bucket.rb', line 151

def status
  @mutex.synchronize do
    now = Time.now.to_f * 1000
    reset_in = if @frozen_at && @remaining <= 0
      [(@frozen_at + @interval - now) / 1000.0, 0].max
    else
      nil
    end

    {
      limit: @limit,
      remaining: @remaining,
      used: @limit - @remaining,
      interval: @interval,
      interval_minutes: @interval / 60000.0,
      frozen_at: @frozen_at ? Time.at(@frozen_at / 1000.0) : nil,
      reset_in_seconds: reset_in,
      globally_paused: @globally_paused,
      request_allowed: request_allowed?,
      warning_threshold: WARNING_THRESHOLD,
      pause_threshold: PAUSE_THRESHOLD,
      healthy: @remaining > WARNING_THRESHOLD
    }
  end
end

#wait_until_request_availablevoid

This method returns an undefined value.

Wait until a request is allowed (not rate limited by invalid requests) Blocks if we’ve hit the invalid request limit or if globally paused



48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/discord_rda/connection/invalid_bucket.rb', line 48

def wait_until_request_available
  @mutex.synchronize do
    # Wait if globally paused
    while @globally_paused
      @logger&.warn('Waiting: Globally paused due to invalid request limit')
      @mutex.unlock
      @pause_condition.wait
      @mutex.lock
    end

    if @remaining <= PAUSE_THRESHOLD && !@globally_paused
      @globally_paused = true
      @logger&.error('GLOBAL PAUSE ACTIVATED: Approaching invalid request limit!', remaining: @remaining)
    end

    if @remaining <= 0 && @frozen_at
      now = Time.now.to_f * 1000
      future = @frozen_at + @interval
      wait_time = [(future - now) / 1000.0, 0].max

      if wait_time > 0
        @logger&.error('Invalid request bucket exhausted! Waiting to prevent 1-hour ban.', wait_seconds: wait_time.round(2))
        @mutex.unlock
        sleep(wait_time)
        @mutex.lock
      end
    end
  end
end