require 'httpclient' require 'xamarin-test-cloud/http/payload' module XamarinTestCloud module HTTP class Error < RuntimeError; ; end # An HTTP client that retries its connection on errors and can time out. # @!visibility private class RetriableClient attr_reader :client # @!visibility private RETRY_ON = [ # Raised for indicating a connection timeout error HTTPClient::ConnectTimeoutError, # Raised for indicating a request sending timeout error HTTPClient::SendTimeoutError, # Raised for indicating a response receiving timeout error. HTTPClient::ReceiveTimeoutError, Errno::ECONNREFUSED, # The server sent a partial response # Errno::ECONNRESET, # # Client sent TCP reset (RST) before server has accepted the # connection requested by client. Errno::ECONNABORTED, ] # @!visibility private HEADER = { 'Content-Type' => 'application/json;charset=utf-8' } # Creates a new retriable client. # # This initializer takes multiple options. If the option is not # documented, it should be considered _private_. You use undocumented # options at your own risk. # # @param [String] endpoint The server to make the HTTP request # on. # @param [Hash] options Control the retry, timeout, and interval. # @option options [Number] :retries (3) How often to retry. # @option options [Number] :timeout (15) How long to wait for a response # before timing out. AKA receive_timeout. # @option options [Number] :send_timeout (120) How long to wait for an # upload. # @option options [Number] :interval (0.5) How long to sleep between # retries. def initialize(endpoint, options = {}) @client = options[:client] || ::HTTPClient.new @endpoint = endpoint @retries = options.fetch(:retries, 3) @timeout = options.fetch(:timeout, 15) @connect_timeout = options.fetch(:connect_timeout, 15) @send_timeout = options.fetch(:send_timeout, 120) @interval = options.fetch(:interval, 0.5) @on_error = {} end # @!visibility private def on_error(type, &block) @on_error[type] = block end # Make an HTTP get request. # # This method takes multiple options. If the option is not documented, # it should be considered _private_. You use undocumented options at # your own risk. # # @param [Calabash::HTTP::Request] request The request. # @param [Hash] options Control the retry, timeout, and interval. # @option options [Number] :retries (5) How often to retry. # @option options [Number] :timeout (5) How long to wait for a response # before timing out. AKA receive_timeout. # @option options [Number] :send_timeout (120) How long to wait for an # upload. # @option options [Number] :interval (0.5) How long to sleep between # retries. def get(request, options={}) request(request, :get, options) end # Make an HTTP post request. # # This method takes multiple options. If the option is not documented, # it should be considered _private_. You use undocumented options at # your own risk. # # @param [Calabash::HTTP::Request] request The request. # @param [Hash] options Control the retry, timeout, and interval. # @option options [Number] :retries (3) How many times to retry. # @option options [Number] :timeout (15) How long to wait for a response # before timing out. AKA receive_timeout. # @option options [Number] :connect_timeout (15) How long to wait for a # connection. # @option options [Number] :send_timeout (120) How long to wait for an # upload. # @option options [Number] :interval (0.5) How long to sleep between # retries. def post(request, options={}) request(request, :post, options) end private def request(request, request_method, options={}) retries = options.fetch(:retries, @retries) timeout = options.fetch(:timeout, @timeout) connect_timeout = options.fetch(:timeout, @connect_timeout) send_timeout = options.fetch(:send_timeout, @send_timeout) interval = options.fetch(:interval, @interval) header = options.fetch(:header, HEADER) start_time = Time.now last_error = nil client = @client.dup client.receive_timeout = timeout client.connect_timeout = connect_timeout client.send_timeout = send_timeout if options.fetch(:user, nil) && options.fetch(:password, nil) client.set_auth(options[:domain], options.fetch(:user), options.fetch(:password)) end retries.times do |i| first_try = i == 0 # Subtract the aggregate time we've spent thus far to make sure we're # not exceeding the request timeout across retries. time_diff = start_time + timeout + send_timeout - Time.now if time_diff <= 0 raise HTTP::Error, "Timeout exceeded" end payload = Payload.generate(request.params) header.merge!(payload.headers) if payload begin return client.send(request_method, @endpoint + request.route, payload.to_s, header) rescue *RETRY_ON => e if first_try if @on_error[e.class] @on_error[e.class].call(@endpoint) end end last_error = e sleep interval end end raise HTTP::Error, last_error end end end end