require 'timeout' require 'net/http' module Scrivito class ContentService class RateLimitExceeded < StandardError def initialize(retry_after) @retry_after = retry_after end attr_reader :retry_after end class MaxSleepTimeReached < StandardError end class Delay def initialize @sleep_count = 0 @total_sleep_time = 0 end def next_sleep_time(min_sleep = 0) sleep_time = [2 ** @sleep_count * 0.5, min_sleep.to_f].max @total_sleep_time += sleep_time raise MaxSleepTimeReached if @total_sleep_time > 40 @sleep_count += 1 sleep_time end end class << self @next_request_not_before = nil def query(path, payload, options={}) timeout = options.fetch(:timeout, ConnectionManager::DEFAULT_TIMEOUT) retry_until_timeout_on_rate_limit_exceeded do request = build_request(path, payload) if @next_request_not_before && @next_request_not_before > (now = Time.now) sleep @next_request_not_before - now end response = nil retry_once_on_network_error do response = connection_manager.request(request, timeout) end handle_response(response) end end # for tests only def forget_retry_after @next_request_not_before = nil end def connection_manager ConnectionManager.instance end private def retry_once_on_network_error retried = false begin yield rescue NetworkError => e raise e if retried retried = true retry end end def retry_until_timeout_on_rate_limit_exceeded delay = Delay.new begin yield rescue RateLimitExceeded => e begin sleep delay.next_sleep_time(e.retry_after) rescue MaxSleepTimeReached raise ::Scrivito::RateLimitExceeded.new('rate limit exceeded', 429) end retry end end def build_request(path, payload) url = "#{Configuration.endpoint_uri.path}/#{path}".squeeze('/') request = Net::HTTP::Post.new(url, request_headers) request.basic_auth('api_token', Configuration.api_key) request.body = MultiJson.dump(payload) request end def handle_response(response) if response.code.first == '2' retry_after = response["Retry-After"] @next_request_not_before = retry_after ? Time.now + retry_after.to_f : nil MultiJson.load(response.body) elsif response.code == "429" raise RateLimitExceeded.new(response["Retry-After"]) elsif response.code == '403' raise AccessDenied.new(response.body) else raise NetworkError.new("Server responded with status code #{response.code}", response.code) end end def request_headers { 'Accept' => 'application/json', 'Content-Type' => 'application/json', } end end end end