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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: efcf798149673758521fcaf3816c0d0bf9ef7a60
4
- data.tar.gz: 2db07e4799e463cb5a43e1282831ed1589a3cc80
3
+ metadata.gz: 8007fc0fdf2984a6cdc35d90dfbdc73a020686d4
4
+ data.tar.gz: a9b99d9d95c268e8be81324cf98ea76427f72c8e
5
5
  SHA512:
6
- metadata.gz: 2634325d8730ee52e83beaa43915ad49602eff032b325066be3a1d8f63a9746d9613fe8d98b5d23225dff7ec2b9c5ff41cc320e6ab69f5f1db8a0171fbed4139
7
- data.tar.gz: 18def3b9cccc4be9f466f1503edfab366b37fac03b145af3e15cc52124430af38c5b709706835c400efe719fd2dec6220164d9b574f69cd72e8018d0c2e6e829
6
+ metadata.gz: bc49c5224b0824b7d5da339467028ce9cb0b61162d3b9d4a18303e3af37fe493d832afa1cfbc65490647cd3c9441580059d5bf43b6247fa10479624685135cd6
7
+ data.tar.gz: 5eb75a205939f44e074c55ad18d55275c4d9672287e2973b28557e965241fcd562af9611874cddb1c71cdbff2f13b5756e4f3416c36ac7d15661612a7c7247f7
@@ -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}>") { client.request(method, url, body: req_body) }
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
- def_unsafe_request :put
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,8 @@
1
+ class HalClient
2
+ class NullLogger
3
+ def info(*_); end
4
+ def debug(*_); end
5
+ def warn(*_); end
6
+ def error(*_); end
7
+ end
8
+ 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
@@ -1,3 +1,3 @@
1
1
  class HalClient
2
- VERSION = "4.1.5"
2
+ VERSION = "4.2.0"
3
3
  end
@@ -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
@@ -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.1.5
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-06-01 00:00:00.000000000 Z
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