Commit 526f3c0d authored by Luke Duncalfe's avatar Luke Duncalfe

Merge branch '334930_use-http-total-timeout-by-default' into 'master'

Use http read total timeout by default

See merge request gitlab-org/gitlab!66487
parents 5e4a0fab 54c83eb4
...@@ -17,7 +17,7 @@ module DependencyProxy ...@@ -17,7 +17,7 @@ module DependencyProxy
file = Tempfile.new file = Tempfile.new
begin begin
file.write(response) file.write(response.body)
file.flush file.flush
yield(success(file: file, digest: response.headers['docker-content-digest'], content_type: response.headers['content-type'])) yield(success(file: file, digest: response.headers['docker-content-digest'], content_type: response.headers['content-type']))
......
...@@ -39,7 +39,7 @@ module AppSec ...@@ -39,7 +39,7 @@ module AppSec
response = Gitlab::HTTP.try_get(PROFILES_DEFINITION_FILE) response = Gitlab::HTTP.try_get(PROFILES_DEFINITION_FILE)
if response&.success? if response&.success?
content = Gitlab::Config::Loader::Yaml.new(response.to_s).load! content = Gitlab::Config::Loader::Yaml.new(response.body).load!
content.fetch(:Profiles, []) content.fetch(:Profiles, [])
else else
......
...@@ -35,7 +35,7 @@ module Gitlab ...@@ -35,7 +35,7 @@ module Gitlab
end end
def access_token def access_token
Gitlab::Json.parse(access_token_create_response)['access_token'] Gitlab::Json.parse(access_token_create_response.body)['access_token']
end end
def verify_otp(otp_code) def verify_otp(otp_code)
......
...@@ -45,7 +45,7 @@ module Gitlab ...@@ -45,7 +45,7 @@ module Gitlab
errors.push("Remote file `#{location}` could not be fetched because of HTTP code `#{response.code}` error!") errors.push("Remote file `#{location}` could not be fetched because of HTTP code `#{response.code}` error!")
end end
response.to_s if errors.none? response.body if errors.none?
end end
end end
end end
......
...@@ -43,16 +43,29 @@ module Gitlab ...@@ -43,16 +43,29 @@ module Gitlab
options options
end end
unless options.has_key?(:use_read_total_timeout) options[:skip_read_total_timeout] = true if options[:skip_read_total_timeout].nil? && options[:stream_body]
if options[:skip_read_total_timeout]
return httparty_perform_request(http_method, path, options_with_timeouts, &block) return httparty_perform_request(http_method, path, options_with_timeouts, &block)
end end
start_time = Gitlab::Metrics::System.monotonic_time start_time = Gitlab::Metrics::System.monotonic_time
read_total_timeout = options.fetch(:timeout, DEFAULT_READ_TOTAL_TIMEOUT) read_total_timeout = options.fetch(:timeout, DEFAULT_READ_TOTAL_TIMEOUT)
tracked_timeout_error = false
httparty_perform_request(http_method, path, options_with_timeouts) do |fragment| httparty_perform_request(http_method, path, options_with_timeouts) do |fragment|
elapsed = Gitlab::Metrics::System.monotonic_time - start_time elapsed = Gitlab::Metrics::System.monotonic_time - start_time
raise ReadTotalTimeout, "Request timed out after #{elapsed} seconds" if elapsed > read_total_timeout
if elapsed > read_total_timeout
error = ReadTotalTimeout.new("Request timed out after #{elapsed} seconds")
raise error if options[:use_read_total_timeout]
unless tracked_timeout_error
Gitlab::ErrorTracking.track_exception(error)
tracked_timeout_error = true
end
end
block.call fragment if block block.call fragment if block
end end
......
...@@ -40,6 +40,14 @@ module Gitlab ...@@ -40,6 +40,14 @@ module Gitlab
@authenticated = result.response.is_a?(Net::HTTPOK) @authenticated = result.response.is_a?(Net::HTTPOK)
store_cookies(result) if options[:use_cookies] store_cookies(result) if options[:use_cookies]
# This is needed to make response.to_s work. HTTParty::Response internal uses a Net::HTTPResponse as @response.
# When a block is used, Net::HTTPResponse#body will be a Net::ReadAdapter instead of a String.
# In this case HTTParty::Response.to_s will default to inspecting the Net::HTTPResponse class instead
# of returning the content of body.
# See https://github.com/jnunemaker/httparty/blob/v0.18.1/lib/httparty/response.rb#L86-L92
# See https://github.com/ruby/net-http/blob/v0.1.1/lib/net/http/response.rb#L346-L350
result.response.body = result.body
result result
end end
......
...@@ -33,7 +33,7 @@ RSpec.describe Gitlab::HTTP do ...@@ -33,7 +33,7 @@ RSpec.describe Gitlab::HTTP do
WebMock.stub_request(:post, /.*/).to_return do |request| WebMock.stub_request(:post, /.*/).to_return do |request|
sleep 0.002.seconds sleep 0.002.seconds
{ body: 'I\m slow', status: 200 } { body: 'I\'m slow', status: 200 }
end end
end end
...@@ -41,25 +41,67 @@ RSpec.describe Gitlab::HTTP do ...@@ -41,25 +41,67 @@ RSpec.describe Gitlab::HTTP do
subject(:request_slow_responder) { described_class.post('http://example.org', **options) } subject(:request_slow_responder) { described_class.post('http://example.org', **options) }
specify do shared_examples 'tracks the timeout but does not raise an error' do
expect { request_slow_responder }.not_to raise_error specify :aggregate_failures do
expect(Gitlab::ErrorTracking).to receive(:track_exception).with(
an_instance_of(Gitlab::HTTP::ReadTotalTimeout)
).once
expect { request_slow_responder }.not_to raise_error
end
it 'still calls the block' do
expect { |b| described_class.post('http://example.org', **options, &b) }.to yield_with_args
end
end end
context 'with use_read_total_timeout option' do shared_examples 'does not track or raise timeout error' do
specify :aggregate_failures do
expect(Gitlab::ErrorTracking).not_to receive(:track_exception)
expect { request_slow_responder }.not_to raise_error
end
end
it_behaves_like 'tracks the timeout but does not raise an error'
context 'and use_read_total_timeout option is truthy' do
let(:options) { { use_read_total_timeout: true } } let(:options) { { use_read_total_timeout: true } }
it 'raises a timeout error' do it 'raises an error' do
expect { request_slow_responder }.to raise_error(Gitlab::HTTP::ReadTotalTimeout, /Request timed out after ?([0-9]*[.])?[0-9]+ seconds/) expect { request_slow_responder }.to raise_error(Gitlab::HTTP::ReadTotalTimeout, /Request timed out after ?([0-9]*[.])?[0-9]+ seconds/)
end end
end
context 'and timeout option' do context 'and timeout option is greater than DEFAULT_READ_TOTAL_TIMEOUT' do
let(:options) { { use_read_total_timeout: true, timeout: 10.seconds } } let(:options) { { timeout: 10.seconds } }
it 'overrides the default timeout when timeout option is present' do it_behaves_like 'does not track or raise timeout error'
expect { request_slow_responder }.not_to raise_error end
end
context 'and stream_body option is truthy' do
let(:options) { { stream_body: true } }
it_behaves_like 'does not track or raise timeout error'
context 'but skip_read_total_timeout option is falsey' do
let(:options) { { stream_body: true, skip_read_total_timeout: false } }
it_behaves_like 'tracks the timeout but does not raise an error'
end end
end end
context 'and skip_read_total_timeout option is truthy' do
let(:options) { { skip_read_total_timeout: true } }
it_behaves_like 'does not track or raise timeout error'
end
context 'and skip_read_total_timeout option is falsely' do
let(:options) { { skip_read_total_timeout: false } }
it_behaves_like 'tracks the timeout but does not raise an error'
end
end end
it 'calls a block' do it 'calls a block' do
......
...@@ -8,7 +8,7 @@ RSpec.describe DependencyProxy::DownloadBlobService do ...@@ -8,7 +8,7 @@ RSpec.describe DependencyProxy::DownloadBlobService do
let(:token) { Digest::SHA256.hexdigest('123') } let(:token) { Digest::SHA256.hexdigest('123') }
let(:blob_sha) { Digest::SHA256.hexdigest('ruby:2.7.0') } let(:blob_sha) { Digest::SHA256.hexdigest('ruby:2.7.0') }
subject { described_class.new(image, blob_sha, token).execute } subject(:download_blob) { described_class.new(image, blob_sha, token).execute }
context 'remote request is successful' do context 'remote request is successful' do
before do before do
...@@ -18,6 +18,21 @@ RSpec.describe DependencyProxy::DownloadBlobService do ...@@ -18,6 +18,21 @@ RSpec.describe DependencyProxy::DownloadBlobService do
it { expect(subject[:status]).to eq(:success) } it { expect(subject[:status]).to eq(:success) }
it { expect(subject[:file]).to be_a(Tempfile) } it { expect(subject[:file]).to be_a(Tempfile) }
it { expect(subject[:file].size).to eq(6) } it { expect(subject[:file].size).to eq(6) }
it 'streams the download' do
expected_options = { headers: anything, stream_body: true }
expect(Gitlab::HTTP).to receive(:perform_request).with(Net::HTTP::Get, anything, expected_options)
download_blob
end
it 'skips read_total_timeout', :aggregate_failures do
stub_const('GitLab::HTTP::DEFAULT_READ_TOTAL_TIMEOUT', 0)
expect(Gitlab::Metrics::System).not_to receive(:monotonic_time)
expect(download_blob).to include(status: :success)
end
end end
context 'remote request is not found' do context 'remote request is not found' do
......
...@@ -90,6 +90,21 @@ RSpec.describe Projects::LfsPointers::LfsDownloadService do ...@@ -90,6 +90,21 @@ RSpec.describe Projects::LfsPointers::LfsDownloadService do
expect(File.binread(LfsObject.first.file.file.file)).to eq lfs_content expect(File.binread(LfsObject.first.file.file.file)).to eq lfs_content
end end
it 'streams the download' do
expected_options = { headers: anything, stream_body: true }
expect(Gitlab::HTTP).to receive(:perform_request).with(Net::HTTP::Get, anything, expected_options)
subject.execute
end
it 'skips read_total_timeout', :aggregate_failures do
stub_const('GitLab::HTTP::DEFAULT_READ_TOTAL_TIMEOUT', 0)
expect(Gitlab::Metrics::System).not_to receive(:monotonic_time)
expect(subject.execute).to include(status: :success)
end
end end
context 'when file download fails' do context 'when file download fails' do
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment