hal-client 4.1.5 → 4.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|