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 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