http 5.2.0 → 5.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop/rspec.yml +9 -0
- data/.rubocop_todo.yml +45 -32
- data/CHANGELOG.md +17 -1
- data/Gemfile +1 -0
- data/http.gemspec +7 -2
- data/lib/http/base64.rb +12 -0
- data/lib/http/chainable.rb +27 -3
- data/lib/http/connection.rb +4 -2
- data/lib/http/errors.rb +16 -0
- data/lib/http/feature.rb +2 -1
- data/lib/http/features/raise_error.rb +22 -0
- data/lib/http/headers/normalizer.rb +69 -0
- data/lib/http/headers.rb +26 -40
- data/lib/http/request/writer.rb +2 -1
- data/lib/http/request.rb +3 -2
- data/lib/http/retriable/client.rb +37 -0
- data/lib/http/retriable/delay_calculator.rb +64 -0
- data/lib/http/retriable/errors.rb +14 -0
- data/lib/http/retriable/performer.rb +153 -0
- data/lib/http/version.rb +1 -1
- data/lib/http.rb +1 -0
- data/spec/lib/http/features/raise_error_spec.rb +62 -0
- data/spec/lib/http/headers/normalizer_spec.rb +52 -0
- data/spec/lib/http/redirector_spec.rb +6 -5
- data/spec/lib/http/retriable/delay_calculator_spec.rb +69 -0
- data/spec/lib/http/retriable/performer_spec.rb +302 -0
- data/spec/lib/http_spec.rb +29 -0
- data/spec/spec_helper.rb +1 -0
- data/spec/support/dummy_server/servlet.rb +13 -0
- data/spec/support/dummy_server.rb +2 -1
- metadata +21 -19
@@ -0,0 +1,153 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "date"
|
4
|
+
require "http/retriable/errors"
|
5
|
+
require "http/retriable/delay_calculator"
|
6
|
+
require "openssl"
|
7
|
+
|
8
|
+
module HTTP
|
9
|
+
module Retriable
|
10
|
+
# Request performing watchdog.
|
11
|
+
# @api private
|
12
|
+
class Performer
|
13
|
+
# Exceptions we should retry
|
14
|
+
RETRIABLE_ERRORS = [
|
15
|
+
HTTP::TimeoutError,
|
16
|
+
HTTP::ConnectionError,
|
17
|
+
IO::EAGAINWaitReadable,
|
18
|
+
Errno::ECONNRESET,
|
19
|
+
Errno::ECONNREFUSED,
|
20
|
+
Errno::EHOSTUNREACH,
|
21
|
+
OpenSSL::SSL::SSLError,
|
22
|
+
EOFError,
|
23
|
+
IOError
|
24
|
+
].freeze
|
25
|
+
|
26
|
+
# @param [Hash] opts
|
27
|
+
# @option opts [#to_i] :tries (5)
|
28
|
+
# @option opts [#call, #to_i] :delay (DELAY_PROC)
|
29
|
+
# @option opts [Array(Exception)] :exceptions (RETRIABLE_ERRORS)
|
30
|
+
# @option opts [Array(#to_i)] :retry_statuses
|
31
|
+
# @option opts [#call] :on_retry
|
32
|
+
# @option opts [#to_f] :max_delay (Float::MAX)
|
33
|
+
# @option opts [#call] :should_retry
|
34
|
+
def initialize(opts)
|
35
|
+
@exception_classes = opts.fetch(:exceptions, RETRIABLE_ERRORS)
|
36
|
+
@retry_statuses = opts[:retry_statuses]
|
37
|
+
@tries = opts.fetch(:tries, 5).to_i
|
38
|
+
@on_retry = opts.fetch(:on_retry, ->(*) {})
|
39
|
+
@should_retry_proc = opts[:should_retry]
|
40
|
+
@delay_calculator = DelayCalculator.new(opts)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Watches request/response execution.
|
44
|
+
#
|
45
|
+
# If any of {RETRIABLE_ERRORS} occur or response status is `5xx`, retries
|
46
|
+
# up to `:tries` amount of times. Sleeps for amount of seconds calculated
|
47
|
+
# with `:delay` proc before each retry.
|
48
|
+
#
|
49
|
+
# @see #initialize
|
50
|
+
# @api private
|
51
|
+
def perform(client, req, &block)
|
52
|
+
1.upto(Float::INFINITY) do |attempt| # infinite loop with index
|
53
|
+
err, res = try_request(&block)
|
54
|
+
|
55
|
+
if retry_request?(req, err, res, attempt)
|
56
|
+
begin
|
57
|
+
wait_for_retry_or_raise(req, err, res, attempt)
|
58
|
+
ensure
|
59
|
+
# Some servers support Keep-Alive on any response. Thus we should
|
60
|
+
# flush response before retry, to avoid state error (when socket
|
61
|
+
# has pending response data and we try to write new request).
|
62
|
+
# Alternatively, as we don't need response body here at all, we
|
63
|
+
# are going to close client, effectivle closing underlying socket
|
64
|
+
# and resetting client's state.
|
65
|
+
client.close
|
66
|
+
end
|
67
|
+
elsif err
|
68
|
+
client.close
|
69
|
+
raise err
|
70
|
+
elsif res
|
71
|
+
return res
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def calculate_delay(iteration, response)
|
77
|
+
@delay_calculator.call(iteration, response)
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
# rubocop:disable Lint/RescueException
|
83
|
+
def try_request
|
84
|
+
err, res = nil
|
85
|
+
|
86
|
+
begin
|
87
|
+
res = yield
|
88
|
+
rescue Exception => e
|
89
|
+
err = e
|
90
|
+
end
|
91
|
+
|
92
|
+
[err, res]
|
93
|
+
end
|
94
|
+
# rubocop:enable Lint/RescueException
|
95
|
+
|
96
|
+
def retry_request?(req, err, res, attempt)
|
97
|
+
if @should_retry_proc
|
98
|
+
@should_retry_proc.call(req, err, res, attempt)
|
99
|
+
elsif err
|
100
|
+
retry_exception?(err)
|
101
|
+
else
|
102
|
+
retry_response?(res)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def retry_exception?(err)
|
107
|
+
@exception_classes.any? { |e| err.is_a?(e) }
|
108
|
+
end
|
109
|
+
|
110
|
+
def retry_response?(res)
|
111
|
+
return false unless @retry_statuses
|
112
|
+
|
113
|
+
response_status = res.status.to_i
|
114
|
+
retry_matchers = [@retry_statuses].flatten
|
115
|
+
|
116
|
+
retry_matchers.any? do |matcher|
|
117
|
+
case matcher
|
118
|
+
when Range then matcher.cover?(response_status)
|
119
|
+
when Numeric then matcher == response_status
|
120
|
+
else matcher.call(response_status)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def wait_for_retry_or_raise(req, err, res, attempt)
|
126
|
+
if attempt < @tries
|
127
|
+
@on_retry.call(req, err, res)
|
128
|
+
sleep calculate_delay(attempt, res)
|
129
|
+
else
|
130
|
+
res&.flush
|
131
|
+
raise out_of_retries_error(req, res, err)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
# Builds OutOfRetriesError
|
136
|
+
#
|
137
|
+
# @param request [HTTP::Request]
|
138
|
+
# @param status [HTTP::Response, nil]
|
139
|
+
# @param exception [Exception, nil]
|
140
|
+
def out_of_retries_error(request, response, exception)
|
141
|
+
message = "#{request.verb.to_s.upcase} <#{request.uri}> failed"
|
142
|
+
|
143
|
+
message += " with #{response.status}" if response
|
144
|
+
message += ":#{exception}" if exception
|
145
|
+
|
146
|
+
HTTP::OutOfRetriesError.new(message).tap do |ex|
|
147
|
+
ex.cause = exception
|
148
|
+
ex.response = response
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
data/lib/http/version.rb
CHANGED
data/lib/http.rb
CHANGED
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.describe HTTP::Features::RaiseError do
|
4
|
+
subject(:feature) { described_class.new(ignore: ignore) }
|
5
|
+
|
6
|
+
let(:connection) { double }
|
7
|
+
let(:status) { 200 }
|
8
|
+
let(:ignore) { [] }
|
9
|
+
|
10
|
+
describe "#wrap_response" do
|
11
|
+
subject(:result) { feature.wrap_response(response) }
|
12
|
+
|
13
|
+
let(:response) do
|
14
|
+
HTTP::Response.new(
|
15
|
+
version: "1.1",
|
16
|
+
status: status,
|
17
|
+
headers: {},
|
18
|
+
connection: connection,
|
19
|
+
request: HTTP::Request.new(verb: :get, uri: "https://example.com")
|
20
|
+
)
|
21
|
+
end
|
22
|
+
|
23
|
+
context "when status is 200" do
|
24
|
+
it "returns original request" do
|
25
|
+
expect(result).to be response
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
context "when status is 399" do
|
30
|
+
let(:status) { 399 }
|
31
|
+
|
32
|
+
it "returns original request" do
|
33
|
+
expect(result).to be response
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
context "when status is 400" do
|
38
|
+
let(:status) { 400 }
|
39
|
+
|
40
|
+
it "raises" do
|
41
|
+
expect { result }.to raise_error(HTTP::StatusError, "Unexpected status code 400")
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
context "when status is 599" do
|
46
|
+
let(:status) { 599 }
|
47
|
+
|
48
|
+
it "raises" do
|
49
|
+
expect { result }.to raise_error(HTTP::StatusError, "Unexpected status code 599")
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
context "when error status is ignored" do
|
54
|
+
let(:status) { 500 }
|
55
|
+
let(:ignore) { [500] }
|
56
|
+
|
57
|
+
it "returns original request" do
|
58
|
+
expect(result).to be response
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.describe HTTP::Headers::Normalizer do
|
4
|
+
subject(:normalizer) { described_class.new }
|
5
|
+
|
6
|
+
include_context RSpec::Memory
|
7
|
+
|
8
|
+
describe "#call" do
|
9
|
+
it "normalizes the header" do
|
10
|
+
expect(normalizer.call("content_type")).to eq "Content-Type"
|
11
|
+
end
|
12
|
+
|
13
|
+
it "returns a non-frozen string" do
|
14
|
+
expect(normalizer.call("content_type")).not_to be_frozen
|
15
|
+
end
|
16
|
+
|
17
|
+
it "evicts the oldest item when cache is full" do
|
18
|
+
max_headers = (1..described_class::Cache::MAX_SIZE).map { |i| "Header#{i}" }
|
19
|
+
max_headers.each { |header| normalizer.call(header) }
|
20
|
+
normalizer.call("New-Header")
|
21
|
+
cache_store = normalizer.instance_variable_get(:@cache).instance_variable_get(:@store)
|
22
|
+
expect(cache_store.keys).to eq(max_headers[1..] + ["New-Header"])
|
23
|
+
end
|
24
|
+
|
25
|
+
it "retuns mutable strings" do
|
26
|
+
normalized_headers = Array.new(3) { normalizer.call("content_type") }
|
27
|
+
|
28
|
+
expect(normalized_headers)
|
29
|
+
.to satisfy { |arr| arr.uniq.size == 1 }
|
30
|
+
.and(satisfy { |arr| arr.map(&:object_id).uniq.size == normalized_headers.size })
|
31
|
+
.and(satisfy { |arr| arr.none?(&:frozen?) })
|
32
|
+
end
|
33
|
+
|
34
|
+
it "allocates minimal memory for normalization of the same header" do
|
35
|
+
normalizer.call("accept") # XXX: Ensure normalizer is pre-allocated
|
36
|
+
|
37
|
+
# On first call it is expected to allocate during normalization
|
38
|
+
expect { normalizer.call("content_type") }.to limit_allocations(
|
39
|
+
Array => 1,
|
40
|
+
MatchData => 1,
|
41
|
+
String => 6
|
42
|
+
)
|
43
|
+
|
44
|
+
# On subsequent call it is expected to only allocate copy of a cached string
|
45
|
+
expect { normalizer.call("content_type") }.to limit_allocations(
|
46
|
+
Array => 0,
|
47
|
+
MatchData => 0,
|
48
|
+
String => 1
|
49
|
+
)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -115,12 +115,13 @@ RSpec.describe HTTP::Redirector do
|
|
115
115
|
expect(req_cookie).to eq request_cookies.shift
|
116
116
|
hops.shift
|
117
117
|
end
|
118
|
+
|
118
119
|
expect(res.to_s).to eq "bar"
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
120
|
+
expect(res.cookies.cookies.to_h { |c| [c.name, c.value] }).to eq({
|
121
|
+
"foo" => "42",
|
122
|
+
"bar" => "53",
|
123
|
+
"baz" => "65"
|
124
|
+
})
|
124
125
|
end
|
125
126
|
|
126
127
|
it "returns original cookies in response" do
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.describe HTTP::Retriable::DelayCalculator do
|
4
|
+
let(:response) do
|
5
|
+
HTTP::Response.new(
|
6
|
+
status: 200,
|
7
|
+
version: "1.1",
|
8
|
+
headers: {},
|
9
|
+
body: "Hello world!",
|
10
|
+
request: HTTP::Request.new(verb: :get, uri: "http://example.com")
|
11
|
+
)
|
12
|
+
end
|
13
|
+
|
14
|
+
def call_delay(iterations, **options)
|
15
|
+
described_class.new(options).call(iterations, response)
|
16
|
+
end
|
17
|
+
|
18
|
+
def call_retry_header(value, **options)
|
19
|
+
response.headers["Retry-After"] = value
|
20
|
+
described_class.new(options).call(rand(1...100), response)
|
21
|
+
end
|
22
|
+
|
23
|
+
it "prevents negative sleep time" do
|
24
|
+
expect(call_delay(20, delay: -20)).to eq 0
|
25
|
+
end
|
26
|
+
|
27
|
+
it "backs off exponentially" do
|
28
|
+
expect(call_delay(1)).to be_between 0, 1
|
29
|
+
expect(call_delay(2)).to be_between 1, 2
|
30
|
+
expect(call_delay(3)).to be_between 3, 4
|
31
|
+
expect(call_delay(4)).to be_between 7, 8
|
32
|
+
expect(call_delay(5)).to be_between 15, 16
|
33
|
+
end
|
34
|
+
|
35
|
+
it "can have a maximum wait time" do
|
36
|
+
expect(call_delay(1, max_delay: 5)).to be_between 0, 1
|
37
|
+
expect(call_delay(5, max_delay: 5)).to eq 5
|
38
|
+
end
|
39
|
+
|
40
|
+
it "respects Retry-After headers as integer" do
|
41
|
+
delay_time = rand(6...2500)
|
42
|
+
header_value = delay_time.to_s
|
43
|
+
expect(call_retry_header(header_value)).to eq delay_time
|
44
|
+
expect(call_retry_header(header_value, max_delay: 5)).to eq 5
|
45
|
+
end
|
46
|
+
|
47
|
+
it "respects Retry-After headers as rfc2822 timestamp" do
|
48
|
+
delay_time = rand(6...2500)
|
49
|
+
header_value = (Time.now.gmtime + delay_time).to_datetime.rfc2822.sub("+0000", "GMT")
|
50
|
+
expect(call_retry_header(header_value)).to be_within(1).of(delay_time)
|
51
|
+
expect(call_retry_header(header_value, max_delay: 5)).to eq 5
|
52
|
+
end
|
53
|
+
|
54
|
+
it "respects Retry-After headers as rfc2822 timestamp in the past" do
|
55
|
+
delay_time = rand(6...2500)
|
56
|
+
header_value = (Time.now.gmtime - delay_time).to_datetime.rfc2822.sub("+0000", "GMT")
|
57
|
+
expect(call_retry_header(header_value)).to eq 0
|
58
|
+
end
|
59
|
+
|
60
|
+
it "does not error on invalid Retry-After header" do
|
61
|
+
[ # invalid strings
|
62
|
+
"This is a string with a number 5 in it",
|
63
|
+
"8 Eight is the first digit in this string",
|
64
|
+
"This is a string with a #{Time.now.gmtime.to_datetime.rfc2822} timestamp in it"
|
65
|
+
].each do |header_value|
|
66
|
+
expect(call_retry_header(header_value)).to eq 0
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,302 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.describe HTTP::Retriable::Performer do
|
4
|
+
let(:client) do
|
5
|
+
HTTP::Client.new
|
6
|
+
end
|
7
|
+
|
8
|
+
let(:response) do
|
9
|
+
HTTP::Response.new(
|
10
|
+
status: 200,
|
11
|
+
version: "1.1",
|
12
|
+
headers: {},
|
13
|
+
body: "Hello world!",
|
14
|
+
request: request
|
15
|
+
)
|
16
|
+
end
|
17
|
+
|
18
|
+
let(:request) do
|
19
|
+
HTTP::Request.new(
|
20
|
+
verb: :get,
|
21
|
+
uri: "http://example.com"
|
22
|
+
)
|
23
|
+
end
|
24
|
+
|
25
|
+
let(:perform_spy) { { counter: 0 } }
|
26
|
+
let(:counter_spy) { perform_spy[:counter] }
|
27
|
+
|
28
|
+
before do
|
29
|
+
stub_const("CustomException", Class.new(StandardError))
|
30
|
+
end
|
31
|
+
|
32
|
+
def perform(options = {}, client_arg = client, request_arg = request, &block)
|
33
|
+
# by explicitly overwriting the default delay, we make a much faster test suite
|
34
|
+
default_options = { delay: 0 }
|
35
|
+
options = default_options.merge(options)
|
36
|
+
|
37
|
+
HTTP::Retriable::Performer
|
38
|
+
.new(options)
|
39
|
+
.perform(client_arg, request_arg) do
|
40
|
+
perform_spy[:counter] += 1
|
41
|
+
block ? yield : response
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def measure_wait
|
46
|
+
t1 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
47
|
+
result = yield
|
48
|
+
t2 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
49
|
+
[t2 - t1, result]
|
50
|
+
end
|
51
|
+
|
52
|
+
describe "#perform" do
|
53
|
+
describe "expected exception" do
|
54
|
+
it "retries the request" do
|
55
|
+
expect do
|
56
|
+
perform(exceptions: [CustomException], tries: 2) do
|
57
|
+
raise CustomException
|
58
|
+
end
|
59
|
+
end.to raise_error HTTP::OutOfRetriesError
|
60
|
+
|
61
|
+
expect(counter_spy).to eq 2
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
describe "unexpected exception" do
|
66
|
+
it "does not retry the request" do
|
67
|
+
expect do
|
68
|
+
perform(exceptions: [], tries: 2) do
|
69
|
+
raise CustomException
|
70
|
+
end
|
71
|
+
end.to raise_error CustomException
|
72
|
+
|
73
|
+
expect(counter_spy).to eq 1
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
describe "expected status codes" do
|
78
|
+
def response(**options)
|
79
|
+
HTTP::Response.new(
|
80
|
+
{
|
81
|
+
status: 200,
|
82
|
+
version: "1.1",
|
83
|
+
headers: {},
|
84
|
+
body: "Hello world!",
|
85
|
+
request: request
|
86
|
+
}.merge(options)
|
87
|
+
)
|
88
|
+
end
|
89
|
+
|
90
|
+
it "retries the request" do
|
91
|
+
expect do
|
92
|
+
perform(retry_statuses: [200], tries: 2)
|
93
|
+
end.to raise_error HTTP::OutOfRetriesError
|
94
|
+
|
95
|
+
expect(counter_spy).to eq 2
|
96
|
+
end
|
97
|
+
|
98
|
+
describe "status codes can be expressed in many ways" do
|
99
|
+
[
|
100
|
+
301,
|
101
|
+
[200, 301, 485],
|
102
|
+
250...400,
|
103
|
+
[250...Float::INFINITY],
|
104
|
+
->(status_code) { status_code == 301 },
|
105
|
+
[->(status_code) { status_code == 301 }]
|
106
|
+
].each do |retry_statuses|
|
107
|
+
it retry_statuses.to_s do
|
108
|
+
expect do
|
109
|
+
perform(retry_statuses: retry_statuses, tries: 2) do
|
110
|
+
response(status: 301)
|
111
|
+
end
|
112
|
+
end.to raise_error HTTP::OutOfRetriesError
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
describe "unexpected status code" do
|
119
|
+
it "does not retry the request" do
|
120
|
+
expect(
|
121
|
+
perform(retry_statuses: [], tries: 2)
|
122
|
+
).to eq response
|
123
|
+
|
124
|
+
expect(counter_spy).to eq 1
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
describe "on_retry callback" do
|
129
|
+
it "calls the on_retry callback on each retry with exception" do
|
130
|
+
callback_call_spy = 0
|
131
|
+
|
132
|
+
callback_spy = proc do |callback_request, error, callback_response|
|
133
|
+
expect(callback_request).to eq request
|
134
|
+
expect(error).to be_a HTTP::TimeoutError
|
135
|
+
expect(callback_response).to be_nil
|
136
|
+
callback_call_spy += 1
|
137
|
+
end
|
138
|
+
|
139
|
+
expect do
|
140
|
+
perform(tries: 3, on_retry: callback_spy) do
|
141
|
+
raise HTTP::TimeoutError
|
142
|
+
end
|
143
|
+
end.to raise_error HTTP::OutOfRetriesError
|
144
|
+
|
145
|
+
expect(callback_call_spy).to eq 2
|
146
|
+
end
|
147
|
+
|
148
|
+
it "calls the on_retry callback on each retry with response" do
|
149
|
+
callback_call_spy = 0
|
150
|
+
|
151
|
+
callback_spy = proc do |callback_request, error, callback_response|
|
152
|
+
expect(callback_request).to eq request
|
153
|
+
expect(error).to be_nil
|
154
|
+
expect(callback_response).to be response
|
155
|
+
callback_call_spy += 1
|
156
|
+
end
|
157
|
+
|
158
|
+
expect do
|
159
|
+
perform(retry_statuses: [200], tries: 3, on_retry: callback_spy)
|
160
|
+
end.to raise_error HTTP::OutOfRetriesError
|
161
|
+
|
162
|
+
expect(callback_call_spy).to eq 2
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
describe "delay option" do
|
167
|
+
let(:timing_slack) { 0.05 }
|
168
|
+
|
169
|
+
it "can be a positive number" do
|
170
|
+
time, = measure_wait do
|
171
|
+
perform(delay: 0.1, tries: 3, should_retry: ->(*) { true })
|
172
|
+
rescue HTTP::OutOfRetriesError
|
173
|
+
end
|
174
|
+
expect(time).to be_within(timing_slack).of(0.2)
|
175
|
+
end
|
176
|
+
|
177
|
+
it "can be a proc number" do
|
178
|
+
time, = measure_wait do
|
179
|
+
perform(delay: ->(attempt) { attempt / 10.0 }, tries: 3, should_retry: ->(*) { true })
|
180
|
+
rescue HTTP::OutOfRetriesError
|
181
|
+
end
|
182
|
+
expect(time).to be_within(timing_slack).of(0.3)
|
183
|
+
end
|
184
|
+
|
185
|
+
it "receives correct retry number when a proc" do
|
186
|
+
retry_count = 0
|
187
|
+
retry_proc = proc do |attempt|
|
188
|
+
expect(attempt).to eq(retry_count).and(be > 0)
|
189
|
+
0
|
190
|
+
end
|
191
|
+
begin
|
192
|
+
perform(delay: retry_proc, should_retry: ->(*) { true }) do
|
193
|
+
retry_count += 1
|
194
|
+
response
|
195
|
+
end
|
196
|
+
rescue HTTP::OutOfRetriesError
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
describe "should_retry option" do
|
202
|
+
it "decides if the request should be retried" do # rubocop:disable RSpec/MultipleExpectations
|
203
|
+
retry_proc = proc do |req, err, res, attempt|
|
204
|
+
expect(req).to eq request
|
205
|
+
if res
|
206
|
+
expect(err).to be_nil
|
207
|
+
expect(res).to be response
|
208
|
+
else
|
209
|
+
expect(err).to be_a CustomException
|
210
|
+
expect(res).to be_nil
|
211
|
+
end
|
212
|
+
|
213
|
+
attempt < 5
|
214
|
+
end
|
215
|
+
|
216
|
+
begin
|
217
|
+
perform(should_retry: retry_proc) do
|
218
|
+
rand < 0.5 ? response : raise(CustomException)
|
219
|
+
end
|
220
|
+
rescue CustomException
|
221
|
+
end
|
222
|
+
|
223
|
+
expect(counter_spy).to eq 5
|
224
|
+
end
|
225
|
+
|
226
|
+
it "raises the original error if not retryable" do
|
227
|
+
retry_proc = ->(*) { false }
|
228
|
+
|
229
|
+
expect do
|
230
|
+
perform(should_retry: retry_proc) do
|
231
|
+
raise CustomException
|
232
|
+
end
|
233
|
+
end.to raise_error CustomException
|
234
|
+
|
235
|
+
expect(counter_spy).to eq 1
|
236
|
+
end
|
237
|
+
|
238
|
+
it "raises HTTP::OutOfRetriesError if retryable" do
|
239
|
+
retry_proc = ->(*) { true }
|
240
|
+
|
241
|
+
expect do
|
242
|
+
perform(should_retry: retry_proc) do
|
243
|
+
raise CustomException
|
244
|
+
end
|
245
|
+
end.to raise_error HTTP::OutOfRetriesError
|
246
|
+
|
247
|
+
expect(counter_spy).to eq 5
|
248
|
+
end
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
describe "connection closing" do
|
253
|
+
let(:client) { double(:client) }
|
254
|
+
|
255
|
+
it "does not close the connection if we get a propper response" do
|
256
|
+
expect(client).not_to receive(:close)
|
257
|
+
perform
|
258
|
+
end
|
259
|
+
|
260
|
+
it "closes the connection after each raiseed attempt" do
|
261
|
+
expect(client).to receive(:close).exactly(3).times
|
262
|
+
begin
|
263
|
+
perform(should_retry: ->(*) { true }, tries: 3)
|
264
|
+
rescue HTTP::OutOfRetriesError
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
it "closes the connection on an unexpected exception" do
|
269
|
+
expect(client).to receive(:close)
|
270
|
+
begin
|
271
|
+
perform do
|
272
|
+
raise CustomException
|
273
|
+
end
|
274
|
+
rescue CustomException
|
275
|
+
end
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
describe HTTP::OutOfRetriesError do
|
280
|
+
it "has the original exception as a cause if available" do
|
281
|
+
err = nil
|
282
|
+
begin
|
283
|
+
perform(exceptions: [CustomException]) do
|
284
|
+
raise CustomException
|
285
|
+
end
|
286
|
+
rescue described_class => e
|
287
|
+
err = e
|
288
|
+
end
|
289
|
+
expect(err.cause).to be_a CustomException
|
290
|
+
end
|
291
|
+
|
292
|
+
it "has the last raiseed response as an attribute" do
|
293
|
+
err = nil
|
294
|
+
begin
|
295
|
+
perform(should_retry: ->(*) { true })
|
296
|
+
rescue described_class => e
|
297
|
+
err = e
|
298
|
+
end
|
299
|
+
expect(err.response).to be response
|
300
|
+
end
|
301
|
+
end
|
302
|
+
end
|