lib/bearcat/client.rb in bearcat-1.4.13 vs lib/bearcat/client.rb in bearcat-1.5.0.beta1
- old
+ new
@@ -1,76 +1,104 @@
require 'active_support/core_ext/hash'
require 'footrest/client'
-require 'paul_walker'
+require_relative 'rate_limiting'
+require_relative 'client_module'
module Bearcat
class Client < Footrest::Client
require 'bearcat/api_array'
+ @added_modules = []
+
Dir[File.join(__dir__, 'client', '*.rb')].each do |mod|
mname = File.basename(mod, '.*').camelize
mname = 'GraphQL' if mname == 'GraphQl'
require mod
- include "Bearcat::Client::#{mname}".constantize
+ lmod = "Bearcat::Client::#{mname}".constantize
+ include lmod
+ @added_modules << lmod
end
- # Override Footrest request for ApiArray support
+ def self.registered_endpoints
+ @registered_endpoints ||= @added_modules.reduce({}) do |h, m|
+ h.merge!(m._registered_endpoints) rescue h
+ end
+ @registered_endpoints
+ end
+
def request(method, &block)
- enforce_rate_limits
- response = connection.send(method, &block)
- apply_rate_limits(response.headers['x-rate-limit-remaining'])
+ response = rate_limited_request do
+ connection.send(method, &block)
+ end
ApiArray.process_response(response, self)
end
- def enforce_rate_limits
- return unless limit_remaining.present?
- return unless limit_remaining < Bearcat.rate_limit_min
+ protected
- tts = ((Bearcat.rate_limit_min - limit_remaining) / 5).ceil
- tts = Bearcat.min_sleep_seconds if tts < Bearcat.min_sleep_seconds
- tts = Bearcat.max_sleep_seconds if tts > Bearcat.max_sleep_seconds
+ def rate_limited_request
+ return yield unless rate_limiter
+ canvas_rate_limits= 0
+ response = nil
- message = "Canvas API rate limit minimum #{Bearcat.rate_limit_min} reached. "\
- "Sleeping for #{tts} second(s) to catch up ~zzZZ~. "\
- "Limit Remaining: #{limit_remaining}"
- Bearcat.logger.debug(message)
+ begin
+ rate_limiter.apply(
+ rate_limit_key,
+ max_sleep: Bearcat.max_sleep_seconds,
+ on_sleep: ->(tts) {
+ message = "Canvas API rate limit reached; sleeping for #{tts.to_i} second(s) to catch up."
+ Bearcat.logger.debug(message)
+ },
+ ) do
+ response = yield
+ 0
+ end
+ rescue Footrest::HttpError::Forbidden => err
+ # Somehow our rate-limiting didn't limit enough and Canvas stopped us.
+ response = err.response
+ if canvas_rate_limits < 2 && err.message.include?("(Rate Limit Exceeded)")
+ canvas_rate_limits += 1
+ rate_limiter.checkin_known(rate_limit_key, 0)
- sleep(tts)
- end
+ message = "Canvas API applied rate limit; upticking Bearcat rate-limit avoidance and retrying (Retry #{canvas_rate_limits})."
+ Bearcat.logger.debug(message)
- def apply_rate_limits(limit)
- return if limit.nil?
- self.limit_remaining = limit.to_i
- end
+ retry
+ end
+ raise
+ ensure
+ headers = response.try(:response_headers) || response.try(:headers) || {}
+ # -50 to provide a little extra leeway and hopefully be more proactive, making sure we don't even get close to Canvas throwing a 403, even if an out-of-band process is involved
+ rate_limiter.checkin_known(rate_limit_key, headers['x-rate-limit-remaining'].to_f - 100) if response
+ end
- def using_master_rate_limit?
- config[:master_rate_limit].present? || Bearcat.master_rate_limit.present?
+ response
end
- def limit_remaining
- if using_master_rate_limit?
- Bearcat.master_mutex.synchronize do
- limit = PaulWalker::RateLimit.get(config[:token], config[:token])
- if limit.nil?
- PaulWalker::RateLimit.add(config[:token], config[:token], 0, Bearcat::rate_limit_min)
- limit = { current: 0 }.with_indifferent_access
- end
- Bearcat.logger.debug limit['current'].to_s
- limit['current']
+ def rate_limiter
+ @rate_limiter ||= begin
+ rl = config[:rate_limiter] || Bearcat.rate_limiter
+ master_rate_limit = config[:master_rate_limit].present? ? config[:master_rate_limit] : Bearcat.master_rate_limit
+
+ if rl.nil? && master_rate_limit.nil? && defined?(Rails) && Rails.env.production?
+ master_rate_limit = true if defined?(::Sidekiq)
end
- else
- Bearcat.rate_limits[config[:token]]
- end
- end
- def limit_remaining=(value)
- if using_master_rate_limit?
- Bearcat.master_mutex.synchronize do
- PaulWalker::RateLimit.add(config[:token], config[:token], value, Bearcat::rate_limit_min)
+ if rl.nil? && master_rate_limit
+ rl = RateLimiting::RedisLimiter
end
- else
- Bearcat.rate_limits[config[:token]] = value
+
+ if rl.is_a?(Class)
+ rl.new()
+ elsif rl.present?
+ rl
+ end
end
+ end
+
+ private
+
+ def rate_limit_key
+ Digest::SHA1.hexdigest(config[:token])
end
end
end