hal-client 4.1.5 → 4.2.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/lib/hal_client.rb +39 -11
- data/lib/hal_client/null_logger.rb +8 -0
- data/lib/hal_client/retryinator.rb +55 -0
- data/lib/hal_client/version.rb +1 -1
- data/spec/hal_client/retryinator_spec.rb +74 -0
- data/spec/hal_client_spec.rb +1 -1
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8007fc0fdf2984a6cdc35d90dfbdc73a020686d4
|
4
|
+
data.tar.gz: a9b99d9d95c268e8be81324cf98ea76427f72c8e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bc49c5224b0824b7d5da339467028ce9cb0b61162d3b9d4a18303e3af37fe493d832afa1cfbc65490647cd3c9441580059d5bf43b6247fa10479624685135cd6
|
7
|
+
data.tar.gz: 5eb75a205939f44e074c55ad18d55275c4d9672287e2973b28557e965241fcd562af9611874cddb1c71cdbff2f13b5756e4f3416c36ac7d15661612a7c7247f7
|
data/lib/hal_client.rb
CHANGED
@@ -21,6 +21,8 @@ class HalClient
|
|
21
21
|
autoload :HttpError, 'hal_client/errors'
|
22
22
|
autoload :HttpClientError, 'hal_client/errors'
|
23
23
|
autoload :HttpServerError, 'hal_client/errors'
|
24
|
+
autoload :NullLogger, 'hal_client/null_logger'
|
25
|
+
autoload :Retryinator, 'hal_client/retryinator'
|
24
26
|
|
25
27
|
autoload :RepresentationEditor, 'hal_client/representation_editor'
|
26
28
|
|
@@ -50,6 +52,9 @@ class HalClient
|
|
50
52
|
@logger = options.fetch(:logger, NullLogger.new)
|
51
53
|
@timeout = options.fetch(:timeout, Float::INFINITY)
|
52
54
|
@base_client_with_headers = {}
|
55
|
+
@retry_duration = options.fetch(:retry_duration, Retryinator::DEFAULT_DURATION)
|
56
|
+
|
57
|
+
@retryinator = Retryinator.new(logger: logger, duration: retry_duration)
|
53
58
|
|
54
59
|
default_message_request_headers.set('Accept', options[:accept]) if
|
55
60
|
options[:accept]
|
@@ -93,7 +98,7 @@ class HalClient
|
|
93
98
|
def get(url, headers={})
|
94
99
|
headers = auth_headers(url).merge(headers)
|
95
100
|
client = client_for_get(override_headers: headers)
|
96
|
-
resp = bmtb("GET <#{url}>") { client.get(url) }
|
101
|
+
resp = retryinator.retryable { bmtb("GET <#{url}>") { client.get(url) } }
|
97
102
|
interpret_response resp
|
98
103
|
|
99
104
|
rescue HttpError => e
|
@@ -119,7 +124,36 @@ class HalClient
|
|
119
124
|
|
120
125
|
begin
|
121
126
|
client = client_for_post(override_headers: headers)
|
122
|
-
resp = bmtb("#{verb} <#{url}>") {
|
127
|
+
resp = bmtb("#{verb} <#{url}>") {
|
128
|
+
client.request(method, url, body: req_body)
|
129
|
+
}
|
130
|
+
interpret_response resp
|
131
|
+
|
132
|
+
rescue HttpError => e
|
133
|
+
fail e.class.new("#{verb} <#{url}> failed with code #{e.response.status}", e.response)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def def_idempotent_unsafe_request(method)
|
139
|
+
verb = method.to_s.upcase
|
140
|
+
|
141
|
+
define_method(method) do |url, data, headers={}|
|
142
|
+
headers = auth_headers(url).merge(headers)
|
143
|
+
|
144
|
+
req_body = if data.respond_to? :to_hal
|
145
|
+
data.to_hal
|
146
|
+
elsif data.is_a? Hash
|
147
|
+
data.to_json
|
148
|
+
else
|
149
|
+
data
|
150
|
+
end
|
151
|
+
|
152
|
+
begin
|
153
|
+
client = client_for_post(override_headers: headers)
|
154
|
+
resp = bmtb("#{verb} <#{url}>") {
|
155
|
+
retryinator.retryable { client.request(method, url, body: req_body) }
|
156
|
+
}
|
123
157
|
interpret_response resp
|
124
158
|
|
125
159
|
rescue HttpError => e
|
@@ -141,7 +175,7 @@ class HalClient
|
|
141
175
|
# url - The URL of the resource of interest.
|
142
176
|
# data - a `String`, a `Hash` or an object that responds to `#to_hal`
|
143
177
|
# headers - custom header fields to use for this request
|
144
|
-
|
178
|
+
def_idempotent_unsafe_request :put
|
145
179
|
|
146
180
|
# Patch a `Representation`, `String` or `Hash` to the resource identified at `url`.
|
147
181
|
#
|
@@ -159,7 +193,7 @@ class HalClient
|
|
159
193
|
|
160
194
|
begin
|
161
195
|
client = client_for_post(override_headers: headers)
|
162
|
-
resp = bmtb("DELETE <#{url}>") { client.request(:delete, url) }
|
196
|
+
resp = bmtb("DELETE <#{url}>") { retryinator.retryable { client.request(:delete, url) } }
|
163
197
|
interpret_response resp
|
164
198
|
rescue HttpError => e
|
165
199
|
fail e.class.new("DELETE <#{url}> failed with code #{e.response.status}", e.response)
|
@@ -168,7 +202,7 @@ class HalClient
|
|
168
202
|
|
169
203
|
protected
|
170
204
|
|
171
|
-
attr_reader :headers, :auth_helper, :logger, :timeout
|
205
|
+
attr_reader :headers, :auth_helper, :logger, :timeout, :retry_duration, :retryinator
|
172
206
|
|
173
207
|
NullAuthHelper = ->(_url) { nil }
|
174
208
|
|
@@ -341,10 +375,4 @@ class HalClient
|
|
341
375
|
end
|
342
376
|
end
|
343
377
|
extend EntryPointCovenienceMethods
|
344
|
-
|
345
|
-
|
346
|
-
class NullLogger
|
347
|
-
def info(*_); end
|
348
|
-
def debug(*_); end
|
349
|
-
end
|
350
378
|
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'hal_client/null_logger'
|
2
|
+
|
3
|
+
class HalClient
|
4
|
+
|
5
|
+
# Retries http requests that meet certain conditions -- we want to retry if we got a 500 level
|
6
|
+
# (server) error but not if we got a 400 level (client) error. We want to retry if we rescue an
|
7
|
+
# HttpError (likely a network issue) but not more general exceptions that likely indicate another
|
8
|
+
# problem that should be surfaced.
|
9
|
+
|
10
|
+
# Example usage:
|
11
|
+
# Retryinator.call { fetch_http_response }
|
12
|
+
class Retryinator
|
13
|
+
|
14
|
+
attr_reader :max_tries, :duration, :logger
|
15
|
+
|
16
|
+
DEFAULT_MAX_TRIES = 3
|
17
|
+
DEFAULT_DURATION = 1
|
18
|
+
|
19
|
+
def initialize(options={})
|
20
|
+
@max_tries = options.fetch(:max_tries, DEFAULT_MAX_TRIES)
|
21
|
+
@duration = options.fetch(:duration, DEFAULT_DURATION)
|
22
|
+
@logger = options.fetch(:logger, HalClient::NullLogger.new)
|
23
|
+
end
|
24
|
+
|
25
|
+
def retryable(&block)
|
26
|
+
current_try = 1
|
27
|
+
|
28
|
+
loop do
|
29
|
+
begin
|
30
|
+
result = yield block
|
31
|
+
|
32
|
+
if server_error?(result.code)
|
33
|
+
logger.warn "Received a #{result.code} response with body:\n#{result.body}"
|
34
|
+
return result if current_try >= max_tries
|
35
|
+
else
|
36
|
+
return result
|
37
|
+
end
|
38
|
+
rescue HttpError => e
|
39
|
+
logger.warn "Encountered an HttpError: #{e.message}"
|
40
|
+
raise e if current_try >= max_tries
|
41
|
+
end
|
42
|
+
|
43
|
+
logger.warn "Failed attempt #{current_try} of #{max_tries}. " +
|
44
|
+
"Waiting #{duration} seconds before retrying"
|
45
|
+
|
46
|
+
current_try += 1
|
47
|
+
sleep duration
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def server_error?(status_code)
|
52
|
+
500 <= status_code && status_code < 600
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
data/lib/hal_client/version.rb
CHANGED
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'hal_client/errors'
|
2
|
+
require 'hal_client/retryinator'
|
3
|
+
|
4
|
+
RSpec.describe HalClient::Retryinator do
|
5
|
+
|
6
|
+
let(:max_tries) { 5 }
|
7
|
+
let(:mock_response) { double(Http::Response, body: '', code: 200)}
|
8
|
+
subject { described_class.new(max_tries: max_tries, duration: 0) }
|
9
|
+
|
10
|
+
context "the passed block always raises an error" do
|
11
|
+
let(:never_the_charm) { CharmMaker.new(:never, mock_response) }
|
12
|
+
|
13
|
+
it "raises an error and tries the maximum number of times" do
|
14
|
+
expect do
|
15
|
+
subject.retryable { never_the_charm.call }
|
16
|
+
end.to raise_error(HalClient::HttpError)
|
17
|
+
|
18
|
+
expect(never_the_charm.attempts_made).to eq(max_tries)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
context "the server returns a success response" do
|
23
|
+
let(:first_time_is_the_charm) { CharmMaker.new(1, mock_response) }
|
24
|
+
|
25
|
+
it "returns the response" do
|
26
|
+
expect(subject.retryable {first_time_is_the_charm.call}).to eq(first_time_is_the_charm.response)
|
27
|
+
end
|
28
|
+
|
29
|
+
it "calls the block once" do
|
30
|
+
subject.retryable { first_time_is_the_charm.call }
|
31
|
+
|
32
|
+
expect(first_time_is_the_charm.attempts_made).to eq(1)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
context "the server returns an error response" do
|
37
|
+
before do
|
38
|
+
allow(mock_response).to receive(:code).and_return(500)
|
39
|
+
end
|
40
|
+
|
41
|
+
let(:returns_error_reponses) { CharmMaker.new(1, mock_response) }
|
42
|
+
|
43
|
+
it "retries the request the maximum number of times" do
|
44
|
+
subject.retryable { returns_error_reponses.call }
|
45
|
+
|
46
|
+
expect(returns_error_reponses.attempts_made).to eq(max_tries)
|
47
|
+
end
|
48
|
+
|
49
|
+
it "returns the response" do
|
50
|
+
expect(subject.retryable { returns_error_reponses.call }).to eq(returns_error_reponses.response)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
class CharmMaker
|
55
|
+
attr_accessor :attempts_made, :charm, :response
|
56
|
+
|
57
|
+
def initialize(charm, response)
|
58
|
+
@attempts_made = 0
|
59
|
+
@charm = charm
|
60
|
+
@response = response
|
61
|
+
end
|
62
|
+
|
63
|
+
def call
|
64
|
+
self.attempts_made = self.attempts_made + 1
|
65
|
+
|
66
|
+
if charm == :never || self.attempts_made < charm
|
67
|
+
raise HalClient::HttpError.new("message", response)
|
68
|
+
else
|
69
|
+
response
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
data/spec/hal_client_spec.rb
CHANGED
@@ -7,7 +7,7 @@ RSpec.describe HalClient do
|
|
7
7
|
it { is_expected.to be_kind_of HalClient }
|
8
8
|
end
|
9
9
|
|
10
|
-
subject(:client) { HalClient.new }
|
10
|
+
subject(:client) { HalClient.new(retry_duration: 0) }
|
11
11
|
|
12
12
|
describe '.new w/ custom accept' do
|
13
13
|
subject { HalClient.new(accept: "application/vnd.myspecialmediatype") }
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: hal-client
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 4.
|
4
|
+
version: 4.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Peter Williams
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-
|
11
|
+
date: 2017-10-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: http
|
@@ -175,9 +175,11 @@ files:
|
|
175
175
|
- lib/hal_client/interpreter.rb
|
176
176
|
- lib/hal_client/link.rb
|
177
177
|
- lib/hal_client/links_section.rb
|
178
|
+
- lib/hal_client/null_logger.rb
|
178
179
|
- lib/hal_client/representation.rb
|
179
180
|
- lib/hal_client/representation_editor.rb
|
180
181
|
- lib/hal_client/representation_set.rb
|
182
|
+
- lib/hal_client/retryinator.rb
|
181
183
|
- lib/hal_client/version.rb
|
182
184
|
- spec/hal_client/collection_spec.rb
|
183
185
|
- spec/hal_client/curie_resolver_spec.rb
|
@@ -187,6 +189,7 @@ files:
|
|
187
189
|
- spec/hal_client/representation_editor_spec.rb
|
188
190
|
- spec/hal_client/representation_set_spec.rb
|
189
191
|
- spec/hal_client/representation_spec.rb
|
192
|
+
- spec/hal_client/retryinator_spec.rb
|
190
193
|
- spec/hal_client_spec.rb
|
191
194
|
- spec/spec_helper.rb
|
192
195
|
- spec/support/custom_matchers.rb
|
@@ -223,6 +226,7 @@ test_files:
|
|
223
226
|
- spec/hal_client/representation_editor_spec.rb
|
224
227
|
- spec/hal_client/representation_set_spec.rb
|
225
228
|
- spec/hal_client/representation_spec.rb
|
229
|
+
- spec/hal_client/retryinator_spec.rb
|
226
230
|
- spec/hal_client_spec.rb
|
227
231
|
- spec/spec_helper.rb
|
228
232
|
- spec/support/custom_matchers.rb
|