class DuffelAPI::Middlewares::RateLimiter

Attributes

ratelimit_limit[RW]
ratelimit_remaining[RW]
ratelimit_reset[RW]

Public Class Methods

mutex() click to toggle source
# File lib/duffel_api/middlewares/rate_limiter.rb, line 12
def mutex
  @mutex ||= Mutex.new
end
new(app, options = {}) click to toggle source
Calls superclass method
# File lib/duffel_api/middlewares/rate_limiter.rb, line 17
def initialize(app, options = {})
  super(app, options)
end

Public Instance Methods

call(env) click to toggle source
# File lib/duffel_api/middlewares/rate_limiter.rb, line 21
def call(env)
  sleep_until_ratelimit_reset if rate_limited?

  app.call(env).tap do |response|
    headers = response.env.response_headers

    RateLimiter.mutex.synchronize do
      RateLimiter.ratelimit_limit = new_ratelimit_limit(headers)
      RateLimiter.ratelimit_remaining = new_ratelimit_remaining(headers)
      RateLimiter.ratelimit_reset = new_ratelimit_reset(headers)
    end
  end
end

Private Instance Methods

header_value_with_indifferent_key(headers, key) click to toggle source

NOTE: The check for ‘ratelimit-limit’ vs ‘Ratelimit-Limit’ is required because RSpec and Rails proper handle HTTP headers differently. WebMock converts the header to the latter style, whereas when running through Rails it will be converted to the former.

# File lib/duffel_api/middlewares/rate_limiter.rb, line 41
def header_value_with_indifferent_key(headers, key)
  headers.find { |k, _v| k.casecmp(key).zero? }&.at(1)
end
new_ratelimit_limit(headers) click to toggle source
# File lib/duffel_api/middlewares/rate_limiter.rb, line 45
def new_ratelimit_limit(headers)
  header_value_with_indifferent_key(headers, "ratelimit-limit")&.to_i
end
new_ratelimit_remaining(headers) click to toggle source
# File lib/duffel_api/middlewares/rate_limiter.rb, line 49
def new_ratelimit_remaining(headers)
  header_value_with_indifferent_key(headers, "ratelimit-remaining")&.to_i
end
new_ratelimit_reset(headers) click to toggle source
# File lib/duffel_api/middlewares/rate_limiter.rb, line 53
def new_ratelimit_reset(headers)
  reset_time = header_value_with_indifferent_key(headers, "ratelimit-reset")
  reset_time.nil? ? nil : DateTime.parse(reset_time).to_time
end
rate_limited?() click to toggle source
# File lib/duffel_api/middlewares/rate_limiter.rb, line 58
def rate_limited?
  return false if RateLimiter.ratelimit_reset.nil?
  return false if RateLimiter.ratelimit_remaining.nil?

  RateLimiter.ratelimit_remaining.zero?
end
sleep_until_ratelimit_reset() click to toggle source
# File lib/duffel_api/middlewares/rate_limiter.rb, line 65
def sleep_until_ratelimit_reset
  sleep_time = (RateLimiter.ratelimit_reset.to_i - Time.now.to_i) + 1
  return unless sleep_time.positive?

  ::Logger.new($stdout).info(
    "Duffel rate-limit hit. Sleeping for #{sleep_time} seconds",
  )
  Kernel.sleep(sleep_time)
end