require 'active_support/core_ext/hash' require 'footrest/client' 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 lmod = "Bearcat::Client::#{mname}".constantize include lmod @added_modules << lmod end 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) response = rate_limited_request do connection.send(method, &block) end ApiArray.process_response(response, self) end protected def rate_limited_request return yield unless rate_limiter canvas_rate_limits= 0 response = nil 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) message = "Canvas API applied rate limit; upticking Bearcat rate-limit avoidance and retrying (Retry #{canvas_rate_limits})." Bearcat.logger.debug(message) 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 response end 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 if rl.nil? && master_rate_limit rl = RateLimiting::RedisLimiter end 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