Class: DiscordRDA::ScalableRestClient

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

Overview

Enhanced scalable REST client inspired by Discordeno. Features: request queues, invalid request bucket, URL simplification, proxy support.

Defined Under Namespace

Classes: APIError, BadRequestError, ForbiddenError, NotFoundError, RateLimitedError, ServerError, UnauthorizedError

Constant Summary collapse

API_BASE =

Discord API base URL

'https://discord.com/api/v10'
RATE_LIMIT_REMAINING_HEADER =

Rate limit headers

'x-ratelimit-remaining'
RATE_LIMIT_RESET_AFTER_HEADER =
'x-ratelimit-reset-after'
RATE_LIMIT_GLOBAL_HEADER =
'x-ratelimit-global'
RATE_LIMIT_BUCKET_HEADER =
'x-ratelimit-bucket'
RATE_LIMIT_LIMIT_HEADER =
'x-ratelimit-limit'
MAJOR_PARAMS =

Major parameters that affect rate limit buckets

%w[channels guilds webhooks].freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(config, logger, proxy: nil) ⇒ ScalableRestClient

Initialize the scalable REST client

Parameters:

  • config (Configuration)

    Bot configuration

  • logger (Logger)

    Logger instance

  • proxy (Hash) (defaults to: nil)

    Proxy configuration (base_url, authorization)



61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# File 'lib/discord_rda/connection/scalable_rest_client.rb', line 61

def initialize(config, logger, proxy: nil)
  @config = config
  @logger = logger
  @invalid_bucket = InvalidRequestBucket.new(logger: logger)
  @queues = {}
  @rate_limited_paths = {}
  @globally_rate_limited = false
  @processing_rate_limited_paths = false
  @delete_queue_delay = 60_000
  @max_retry_count = Float::INFINITY
  @mutex = Mutex.new
  @internet = nil

  # Proxy configuration for horizontal scaling
  if proxy
    @is_proxied = true
    @proxy_base_url = proxy[:base_url]
    @proxy_authorization = proxy[:authorization]
  else
    @is_proxied = false
    @proxy_base_url = API_BASE
  end
end

Instance Attribute Details

#configConfiguration (readonly)

Returns Configuration instance.

Returns:



22
23
24
# File 'lib/discord_rda/connection/scalable_rest_client.rb', line 22

def config
  @config
end

#delete_queue_delayInteger (readonly)

Returns Delay before deleting empty queue (ms).

Returns:

  • (Integer)

    Delay before deleting empty queue (ms)



43
44
45
# File 'lib/discord_rda/connection/scalable_rest_client.rb', line 43

def delete_queue_delay
  @delete_queue_delay
end

#globally_rate_limitedBoolean

Returns Whether globally rate limited.

Returns:

  • (Boolean)

    Whether globally rate limited



37
38
39
# File 'lib/discord_rda/connection/scalable_rest_client.rb', line 37

def globally_rate_limited
  @globally_rate_limited
end

#invalid_bucketInvalidRequestBucket (readonly)

Returns Invalid request bucket.

Returns:



28
29
30
# File 'lib/discord_rda/connection/scalable_rest_client.rb', line 28

def invalid_bucket
  @invalid_bucket
end

#is_proxiedBoolean (readonly)

Returns Whether using proxy.

Returns:

  • (Boolean)

    Whether using proxy



49
50
51
# File 'lib/discord_rda/connection/scalable_rest_client.rb', line 49

def is_proxied
  @is_proxied
end

#loggerLogger (readonly)

Returns Logger instance.

Returns:

  • (Logger)

    Logger instance



25
26
27
# File 'lib/discord_rda/connection/scalable_rest_client.rb', line 25

def logger
  @logger
end

#max_retry_countInteger (readonly)

Returns Maximum retry count.

Returns:

  • (Integer)

    Maximum retry count



46
47
48
# File 'lib/discord_rda/connection/scalable_rest_client.rb', line 46

def max_retry_count
  @max_retry_count
end

#processing_rate_limited_pathsBoolean

Returns Whether processing rate limited paths.

Returns:

  • (Boolean)

    Whether processing rate limited paths



40
41
42
# File 'lib/discord_rda/connection/scalable_rest_client.rb', line 40

def processing_rate_limited_paths
  @processing_rate_limited_paths
end

#proxy_authorizationString (readonly)

Returns Proxy authorization.

Returns:

  • (String)

    Proxy authorization



55
56
57
# File 'lib/discord_rda/connection/scalable_rest_client.rb', line 55

def proxy_authorization
  @proxy_authorization
end

#proxy_base_urlString (readonly)

Returns Proxy base URL.

Returns:

  • (String)

    Proxy base URL



52
53
54
# File 'lib/discord_rda/connection/scalable_rest_client.rb', line 52

def proxy_base_url
  @proxy_base_url
end

#queuesHash<String, RequestQueue> (readonly)

Returns Request queues.

Returns:



31
32
33
# File 'lib/discord_rda/connection/scalable_rest_client.rb', line 31

def queues
  @queues
end

#rate_limited_pathsHash<String, Hash> (readonly)

Returns Rate limited paths.

Returns:

  • (Hash<String, Hash>)

    Rate limited paths



34
35
36
# File 'lib/discord_rda/connection/scalable_rest_client.rb', line 34

def rate_limited_paths
  @rate_limited_paths
end

Instance Method Details

#check_rate_limits(url, identifier) ⇒ Integer, false

Check rate limits for a URL or bucket

Parameters:

  • url (String)

    URL or bucket ID

  • identifier (String)

    Queue identifier

Returns:

  • (Integer, false)

    Milliseconds until reset, or false if not limited



186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
# File 'lib/discord_rda/connection/scalable_rest_client.rb', line 186

def check_rate_limits(url, identifier)
  @mutex.synchronize do
    # Check specific URL rate limit
    limited = @rate_limited_paths["#{identifier}#{url}"]
    global = @rate_limited_paths['global']
    now = Time.now.to_f * 1000

    if limited && now < limited[:reset_timestamp]
      return limited[:reset_timestamp] - now
    end

    if global && now < global[:reset_timestamp]
      return global[:reset_timestamp] - now
    end

    false
  end
end

#delete(route, options = {}) ⇒ Hash

Make a DELETE request

Parameters:

  • route (String)

    API route

  • options (Hash) (defaults to: {})

    Request options

Returns:

  • (Hash)

    Response data



135
136
137
# File 'lib/discord_rda/connection/scalable_rest_client.rb', line 135

def delete(route, options = {})
  make_request(:delete, route, options)
end

#get(route, options = {}) ⇒ Hash

Make a GET request

Parameters:

  • route (String)

    API route

  • options (Hash) (defaults to: {})

    Request options

Returns:

  • (Hash)

    Response data



103
104
105
# File 'lib/discord_rda/connection/scalable_rest_client.rb', line 103

def get(route, options = {})
  make_request(:get, route, options)
end

#patch(route, options = {}) ⇒ Hash

Make a PATCH request

Parameters:

  • route (String)

    API route

  • options (Hash) (defaults to: {})

    Request options

Returns:

  • (Hash)

    Response data



127
128
129
# File 'lib/discord_rda/connection/scalable_rest_client.rb', line 127

def patch(route, options = {})
  make_request(:patch, route, options)
end

#post(route, options = {}) ⇒ Hash

Make a POST request

Parameters:

  • route (String)

    API route

  • options (Hash) (defaults to: {})

    Request options

Returns:

  • (Hash)

    Response data



111
112
113
# File 'lib/discord_rda/connection/scalable_rest_client.rb', line 111

def post(route, options = {})
  make_request(:post, route, options)
end

#process_rate_limited_pathsvoid

This method returns an undefined value.

Process rate limited paths (cleanup loop)



207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
# File 'lib/discord_rda/connection/scalable_rest_client.rb', line 207

def process_rate_limited_paths
  @mutex.synchronize do
    now = Time.now.to_f * 1000

    @rate_limited_paths.delete_if do |key, value|
      if value[:reset_timestamp] <= now
        # If it was global, mark as not globally rate limited
        @globally_rate_limited = false if key == 'global'
        true # Delete this entry
      else
        false # Keep this entry
      end
    end

    # If all paths are cleared, stop processing
    if @rate_limited_paths.empty?
      @processing_rate_limited_paths = false
    else
      @processing_rate_limited_paths = true
      # Recheck in 1 second
      Async { sleep(1); process_rate_limited_paths }
    end
  end
end

#put(route, options = {}) ⇒ Hash

Make a PUT request

Parameters:

  • route (String)

    API route

  • options (Hash) (defaults to: {})

    Request options

Returns:

  • (Hash)

    Response data



119
120
121
# File 'lib/discord_rda/connection/scalable_rest_client.rb', line 119

def put(route, options = {})
  make_request(:put, route, options)
end

#simplify_url(url, method) ⇒ String

Simplify URL for rate limit bucket identification

Parameters:

  • url (String)

    Full URL

  • method (Symbol)

    HTTP method

Returns:

  • (String)

    Simplified URL for bucket



143
144
145
146
147
148
149
150
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
176
177
178
179
180
# File 'lib/discord_rda/connection/scalable_rest_client.rb', line 143

def simplify_url(url, method)
  # Split URL into parts
  parts = url.split('/').reject(&:empty?)

  # Build simplified URL
  simplified = [method.to_s.upcase]

  parts.each_with_index do |part, index|
    # Check if this is a major parameter (channels, guilds, webhooks)
    if MAJOR_PARAMS.include?(part)
      simplified << part
      # Keep the ID after major params
      if parts[index + 1] && parts[index + 1] =~ /^\d+$/
        simplified << parts[index + 1]
      end
    elsif part =~ /^\d+$/
      # Replace numeric IDs with 'x' unless after major param
      prev = parts[index - 1]
      simplified << 'x' unless MAJOR_PARAMS.include?(prev)
    else
      simplified << part
    end
  end

  # Special handling for reactions
  if url.include?('/reactions/')
    # Simplify reactions path: /reactions/emoji/@me or /reactions/emoji/user_id
    simplified = simplify_reactions_url(simplified)
  end

  # Special handling for messages
  if url.include?('/messages/')
    # Keep method in front for messages
    simplified = simplify_messages_url(method, parts)
  end

  simplified.join('/')
end

#startvoid

This method returns an undefined value.

Start the REST client



87
88
89
90
# File 'lib/discord_rda/connection/scalable_rest_client.rb', line 87

def start
  @internet = Async::HTTP::Internet.new
  process_rate_limited_paths
end

#stopvoid

This method returns an undefined value.

Stop the REST client



94
95
96
97
# File 'lib/discord_rda/connection/scalable_rest_client.rb', line 94

def stop
  @internet&.close
  @internet = nil
end

#update_token_queues(old_token, new_token) ⇒ void

This method returns an undefined value.

Update token in all queues (for token refresh)

Parameters:

  • old_token (String)

    Old token

  • new_token (String)

    New token



236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
# File 'lib/discord_rda/connection/scalable_rest_client.rb', line 236

def update_token_queues(old_token, new_token)
  @mutex.synchronize do
    old_identifier = "Bearer #{old_token}"
    new_identifier = "Bearer #{new_token}"

    # Update queues
    @queues.delete_if do |key, queue|
      next false unless key.start_with?(old_identifier)

      @queues.delete(key)
      queue.identifier = new_identifier

      new_key = "#{new_identifier}#{queue.url}"
      existing = @queues[new_key]

      if existing
        # Merge queues
        existing.pending.concat(queue.pending)
        queue.pending.clear
        queue.cleanup
        true # Delete old queue
      else
        @queues[new_key] = queue
        false # Don't delete, we moved it
      end
    end

    # Update rate limited paths
    @rate_limited_paths.delete_if do |key, path|
      next false unless key.start_with?(old_identifier)

      @rate_limited_paths["#{new_identifier}#{path[:url]}"] = path

      if path[:bucket_id]
        @rate_limited_paths["#{new_identifier}#{path[:bucket_id]}"] = path
      end

      true # Delete old entry
    end
  end
end