lib/knapsack_pro/client/connection.rb
# frozen_string_literal: true
module KnapsackPro
module Client
class Connection
class ServerError < StandardError; end
TIMEOUT = 15
REQUEST_RETRY_TIMEBOX = 8
def initialize(action)
@action = action
end
def call
send(action.http_method)
end
def success?
return false if !response_body
status = http_response.code.to_i
status >= 200 && status < 500
end
def errors?
!!(response_body && (response_body['errors'] || response_body['error']))
end
def api_code
return unless response_body
response_body['code']
end
def server_error?
status = http_response.code.to_i
status >= 500 && status < 600
end
private
attr_reader :action, :http_response, :response_body
def logger
KnapsackPro.logger
end
def endpoint
KnapsackPro::Config::Env.endpoint
end
def endpoint_url
endpoint + action.endpoint_path
end
def request_hash
action.request_hash
end
def request_body
request_hash.to_json
end
def json_headers
{
'Content-Type' => 'application/json',
'Accept' => 'application/json',
'KNAPSACK-PRO-CLIENT-NAME' => client_name,
'KNAPSACK-PRO-CLIENT-VERSION' => KnapsackPro::VERSION,
'KNAPSACK-PRO-TEST-SUITE-TOKEN' => KnapsackPro::Config::Env.test_suite_token,
'KNAPSACK-PRO-CI-PROVIDER' => KnapsackPro::Config::Env.ci_provider,
}.compact
end
def client_name
[
'knapsack_pro-ruby',
ENV['KNAPSACK_PRO_TEST_RUNNER'],
].compact.join('/')
end
def parse_response_body(body)
return '' if body == '' || body.nil?
JSON.parse(body)
rescue JSON::ParserError
nil
end
def seed
return if @response_body.nil? || @response_body == ''
response_body['build_distribution_id']
end
def has_seed?
!seed.nil?
end
def make_request(&block)
retries ||= 0
@http_response = block.call
@response_body = parse_response_body(http_response.body)
request_uuid = http_response.header['X-Request-Id'] || 'N/A'
logger.debug("#{action.http_method.to_s.upcase} #{endpoint_url}")
logger.debug("API request UUID: #{request_uuid}")
logger.debug("Test suite split seed: #{seed}") if has_seed?
logger.debug('API response:')
if errors?
logger.error(response_body)
else
logger.debug(response_body)
end
if server_error?
raise ServerError.new(response_body)
end
response_body
rescue ServerError, Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::ETIMEDOUT, Errno::EPIPE, EOFError, SocketError, Net::OpenTimeout, Net::ReadTimeout, OpenSSL::SSL::SSLError => e
logger.warn("#{action.http_method.to_s.upcase} #{endpoint_url}")
logger.warn('Request failed due to:')
logger.warn(e.inspect)
retries += 1
if retries < max_request_retries
wait = retries * REQUEST_RETRY_TIMEBOX
print_every = 2 # seconds
(wait / print_every).ceil.times do |i|
if i == 0
logger.warn("Wait for #{wait}s before retrying the request to Knapsack Pro API.")
else
logger.warn("#{wait - i * print_every}s left before retry...")
end
Kernel.sleep(print_every)
end
retry
else
response_body
end
end
def build_http(uri)
http = net_http.new(uri.host, uri.port)
http.use_ssl = (uri.scheme == 'https')
http.open_timeout = TIMEOUT
http.read_timeout = TIMEOUT
http
end
def net_http
if defined?(WebMock::HttpLibAdapters::NetHttpAdapter::OriginalNetHTTP)
WebMock::HttpLibAdapters::NetHttpAdapter::OriginalNetHTTP
else
Net::HTTP
end
end
def post
uri = URI.parse(endpoint_url)
http = build_http(uri)
make_request do
http.post(uri.path, request_body, json_headers)
end
end
def get
uri = URI.parse(endpoint_url)
uri.query = URI.encode_www_form(request_hash)
http = build_http(uri)
make_request do
http.get(uri, json_headers)
end
end
def max_request_retries
# when user defined max request retries
return KnapsackPro::Config::Env.max_request_retries if KnapsackPro::Config::Env.max_request_retries
# when Fallback Mode is disabled then try more attempts to connect to the API
return 6 unless KnapsackPro::Config::Env.fallback_mode_enabled?
# when Regular Mode then try more attempts to connect to the API
# if only one CI node starts Fallback Mode instead of all then we can't guarantee all test files will be run
# https://github.com/KnapsackPro/knapsack_pro-ruby/pull/124
return 6 if KnapsackPro::Config::Env.regular_mode?
# default number of attempts
3
end
end
end
end