px-service-client 1.1.0 → 1.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: eed1491471134440a4e1ca1644420395f15079e4
4
- data.tar.gz: 5f50c16950abc5f20cd7eb7d82deb75f5731782e
3
+ metadata.gz: 260462f7c8c782f9a5a1b7a0996431e426f01f39
4
+ data.tar.gz: 31ffc0fff0138c3d8519d523da451b23e765de0c
5
5
  SHA512:
6
- metadata.gz: cd65c7907b1870c02add4c35a4360df9487b8974b92b9bdfc2a0af941b6f1a2e44870b8b8c409d7235c72a103bc0d64d8828038bcc54457b2949c014b02f2051
7
- data.tar.gz: cf39f661fb5c27d1643b036afd7d3b28028e9dc098186775dbb461a852bd9ae3b201b629685eb364e2e6ae5b768d29c3b1edb0a35f8952123e2fb784bbfd8c02
6
+ metadata.gz: dc00a6aca11761cc581dc3175a322027e8ee5ac8315933a6c72803665efae80bb5794ba5a25a83c0e831d5b01708f469cd84c79cd3cce8262ab71a5885a4e84f
7
+ data.tar.gz: 6aefddddf56c6968920130e3aaacddc3bcdf0ef41677d747e651a00cb8afcfe561e4a0341cbcfedf90df0b68cdb0ff848442991b63fc3c9fac66d09a01f87941
data/README.md CHANGED
@@ -42,8 +42,16 @@ The features are:
42
42
  ```ruby
43
43
  include Px::Service::Client::Caching
44
44
 
45
- self.cache_client = Dalli::Client.new(...)
46
- self.cache_logger = Logger.new(STDOUT) # or Rails.logger, for example
45
+ # Optional
46
+ caching do |config|
47
+ config.cache_strategy = :none
48
+ config.cache_expiry = 30.seconds
49
+ config.max_page = nil
50
+ config.cache_options = {}
51
+ config.cache_options[:policy_group] = 'general'
52
+ config.cache_client = Dalli::Client.new(...)
53
+ config.cache_logger = Logger.new(STDOUT) # or Rails.logger, for example
54
+ end
47
55
  ```
48
56
 
49
57
  Provides client-side response caching of service requests. Responses are cached in memcached (using the provided cache client) in either a *last-resort* or *first-resort* manner.
@@ -55,6 +63,22 @@ to be refreshed probabilistically (rather than on every request).
55
63
  *first-resort* means that the cached value is always used, if present. Requests to the service are only made
56
64
  when the cached value is close to expiry.
57
65
 
66
+ An example of a cached request:
67
+
68
+ ```ruby
69
+ req = subject.make_request(method, url)
70
+ result = subject.cache_request(url) do
71
+ resp = nil
72
+ multi.context do
73
+ resp = multi.do(req)
74
+ end.run
75
+
76
+ resp
77
+ end
78
+ ```
79
+
80
+ `cache_request` expects a block that returns a `RetriableResponseFuture`. It then returns a `Typhoeus::Response`.
81
+
58
82
  #### Px::Service::Client::CircuitBreaker
59
83
 
60
84
  ```ruby
@@ -18,3 +18,4 @@ require "px/service/client/list_response"
18
18
  require "px/service/client/base"
19
19
  require "px/service/client/multiplexer"
20
20
  require "px/service/client/retriable_response_future"
21
+ require "px/service/client/circuit_breaker_retriable_response_future"
@@ -21,13 +21,14 @@ module Px::Service::Client
21
21
  end
22
22
 
23
23
  module ClassMethods
24
- DefaultConfig = Struct.new(:cache_strategy, :cache_expiry, :max_page, :cache_options, :cache_client) do
24
+ DefaultConfig = Struct.new(:cache_strategy, :cache_expiry, :max_page, :cache_options, :cache_logger, :cache_client) do
25
25
  def initialize
26
26
  self.cache_strategy = :none
27
27
  self.cache_expiry = 30.seconds
28
28
  self.max_page = nil
29
29
  self.cache_options = {}
30
30
  self.cache_options[:policy_group] = 'general'
31
+ self.cache_logger = nil
31
32
  self.cache_client = nil
32
33
  end
33
34
  end
@@ -71,13 +72,10 @@ module Px::Service::Client
71
72
 
72
73
  Future.new do
73
74
  begin
74
- if retry_response.is_a?(Future)
75
- resp = retry_response.value!.options
76
- else
77
- resp = retry_response
78
- end
75
+ raise ArgumentError.new('Block did not return a Future.') unless retry_response.is_a?(Future)
76
+ resp = retry_response.value!
79
77
 
80
- entry = CacheEntry.new(config.cache_client, url, policy_group, resp)
78
+ entry = CacheEntry.new(config.cache_client, url, policy_group, resp.options)
81
79
 
82
80
  # Only store a new result if we roll a 0
83
81
  r = rand(refresh_probability)
@@ -85,7 +83,6 @@ module Px::Service::Client
85
83
  resp
86
84
  rescue Px::Service::ServiceError => ex
87
85
  cache_logger.error "Service responded with exception: #{ex.class.name}: #{ex.message}\n#{ex.backtrace.join('\n')}" if cache_logger
88
-
89
86
  entry = CacheEntry.fetch(config.cache_client, url, policy_group)
90
87
  if entry.nil?
91
88
  # Re-raise the error, no cached response
@@ -93,7 +90,7 @@ module Px::Service::Client
93
90
  end
94
91
 
95
92
  entry.touch(expires_in, refresh_window: 1.minute)
96
- entry.data
93
+ Typhoeus::Response.new(HashWithIndifferentAccess.new(entry.data))
97
94
  end
98
95
  end
99
96
  end
@@ -114,7 +111,7 @@ module Px::Service::Client
114
111
  # don't also try to update the cache.
115
112
  entry.touch(expires_in)
116
113
  else
117
- return Future.new { entry.data }
114
+ return Future.new { Typhoeus::Response.new(HashWithIndifferentAccess.new(entry.data)) }
118
115
  end
119
116
  end
120
117
 
@@ -122,13 +119,10 @@ module Px::Service::Client
122
119
 
123
120
  Future.new do
124
121
  begin
125
- if retry_response.is_a?(Future)
126
- resp = retry_response.value!.options
127
- else
128
- resp = retry_response
129
- end
122
+ raise ArgumentError.new('Block did not return a Future.') unless retry_response.is_a?(Future)
123
+ resp = retry_response.value!
130
124
 
131
- entry = CacheEntry.new(config.cache_client, url, policy_group, resp)
125
+ entry = CacheEntry.new(config.cache_client, url, policy_group, resp.options)
132
126
  entry.store(expires_in)
133
127
  resp
134
128
  rescue Px::Service::ServiceError => ex
@@ -143,22 +137,21 @@ module Px::Service::Client
143
137
  # Set the entry to be expired again (but reset the refresh window). This allows the next call to try again
144
138
  # (assuming the circuit breaker is reset) but keeps the value in the cache in the meantime
145
139
  entry.touch(0.seconds)
146
- entry.data
140
+ Typhoeus::Response.new(HashWithIndifferentAccess.new(entry.data))
147
141
  end
148
142
  end
143
+
144
+ rescue ArgumentError => ex
145
+ Future.new { ex }
149
146
  end
150
147
 
151
148
  def no_cache(&block)
152
149
  retry_response = block.call
153
150
 
154
151
  Future.new do
155
- if retry_response.is_a?(Future)
156
- resp = retry_response.value!.options
157
- else
158
- resp = retry_response
159
- end
152
+ raise ArgumentError.new('Block did not return a Future.') unless retry_response.is_a?(Future)
160
153
 
161
- resp
154
+ retry_response.value!
162
155
  end
163
156
  end
164
157
  end
@@ -18,6 +18,8 @@ module Px::Service::Client::Caching
18
18
  ##
19
19
  # Store this entry in the cache with the given expiry.
20
20
  def store(expires_in, refresh_window: 5.minutes)
21
+ raise ArgumentError.new('Cache client has not been set.') unless cache_client.present?
22
+
21
23
  self.expires_at = DateTime.now + expires_in
22
24
 
23
25
  ActiveSupport::Notifications.instrument("store.caching", { url: url, policy_group: policy_group, expires_in: expires_in} ) do
@@ -33,6 +35,8 @@ module Px::Service::Client::Caching
33
35
  ##
34
36
  # Fetch an entry from the cache. Returns the entry if it's present, otherwise returns nil
35
37
  def self.fetch(cache_client, url, policy_group)
38
+ raise ArgumentError.new('Cache client has not been set.') unless cache_client.present?
39
+
36
40
  key_values = nil
37
41
  data_key = cache_key(url, policy_group, :data)
38
42
  meta_key = cache_key(url, policy_group, :meta)
@@ -54,6 +58,8 @@ module Px::Service::Client::Caching
54
58
  ##
55
59
  # Touch this entry in the cache, updating its expiry time but not its data
56
60
  def touch(expires_in, refresh_window: 5.minutes)
61
+ raise ArgumentError.new('Cache client has not been set.') unless cache_client.present?
62
+
57
63
  self.expires_at = DateTime.now + expires_in
58
64
 
59
65
  ActiveSupport::Notifications.instrument("touch.caching", { url: url, policy_group: policy_group, expires_in: expires_in} ) do
@@ -24,38 +24,40 @@ module Px::Service::Client
24
24
  ##
25
25
  # Make the request, respecting the circuit breaker, if configured
26
26
  def make_request_with_breaker(method, uri, query: nil, headers: nil, body: nil)
27
- Future.new do
28
- state = self.class.circuit_state
29
- handler = self.class.circuit_handler
30
-
31
- if handler.is_tripped(state)
32
- handler.logger.debug("handle: breaker is tripped, refusing to execute: #{state}") if handler.logger
33
- begin
34
- handler.on_circuit_open(state)
35
- rescue StandardError => ex
36
- # Wrap and reroute other exceptions, includes CircuitBreaker::CircuitBrokenException
37
- raise Px::Service::ServiceError.new(ex.message, 503), ex, ex.backtrace
38
- end
39
- end
27
+ state = self.class.circuit_state
28
+ handler = self.class.circuit_handler
40
29
 
30
+ if handler.is_tripped(state)
31
+ handler.logger.debug("handle: breaker is tripped, refusing to execute: #{state}") if handler.logger
41
32
  begin
42
- response = make_request_without_breaker(
43
- method,
44
- uri,
45
- query: query,
46
- headers: headers,
47
- body: body,
48
- timeout: handler.invocation_timeout)
49
-
50
- result = response.value!
51
- handler.on_success(state)
52
-
53
- result
54
- rescue Px::Service::ServiceError
55
- handler.on_failure(state)
56
- raise
33
+ handler.on_circuit_open(state)
34
+ rescue StandardError => ex
35
+ # Wrap and reroute other exceptions, includes CircuitBreaker::CircuitBrokenException
36
+ error = Px::Service::ServiceError.new(ex.message, 503)
37
+ return CircuitBreakerRetriableResponseFuture.new(error)
38
+ end
39
+ end
40
+
41
+ retry_request = make_request_without_breaker(
42
+ method,
43
+ uri,
44
+ query: query,
45
+ headers: headers,
46
+ body: body,
47
+ timeout: handler.invocation_timeout)
48
+
49
+ retry_request.request.on_complete do |response|
50
+ # Wait for request to exhaust retries
51
+ if retry_request.completed?
52
+ if response.response_code >= 500 || response.response_code == 0
53
+ handler.on_failure(state)
54
+ else
55
+ handler.on_success(state)
56
+ end
57
57
  end
58
58
  end
59
+
60
+ retry_request
59
61
  end
60
62
 
61
63
  end
@@ -0,0 +1,13 @@
1
+ module Px::Service::Client
2
+ class CircuitBreakerRetriableResponseFuture < RetriableResponseFuture
3
+
4
+ ##
5
+ # Sets the value of a RetriableResponseFuture to the exception
6
+ # raised when opening the circuit breaker.
7
+ def initialize(ex)
8
+ super()
9
+
10
+ complete(ex)
11
+ end
12
+ end
13
+ end
@@ -18,7 +18,7 @@ module Px::Service::Client
18
18
  response = request_or_future
19
19
  if request_or_future.is_a?(Typhoeus::Request)
20
20
  response = RetriableResponseFuture.new(request_or_future, retries: retries)
21
- elsif !request_or_future.is_a?(RetriableResponseFuture)
21
+ elsif !request_or_future.is_a?(RetriableResponseFuture) || request_or_future.completed?
22
22
  return request_or_future
23
23
  end
24
24
 
@@ -1,7 +1,7 @@
1
1
  module Px
2
2
  module Service
3
3
  module Client
4
- VERSION = "1.1.0"
4
+ VERSION = "1.2.0"
5
5
  end
6
6
  end
7
7
  end
@@ -31,6 +31,6 @@ Gem::Specification.new do |spec|
31
31
  spec.add_development_dependency "vcr"
32
32
  spec.add_development_dependency "guard"
33
33
  spec.add_development_dependency "guard-rspec"
34
- spec.add_development_dependency "rspec", "~> 2.14"
34
+ spec.add_development_dependency "rspec", "~> 2.99"
35
35
  spec.add_development_dependency "timecop", "~> 0.5"
36
36
  end
@@ -16,7 +16,7 @@ describe Px::Service::Client::Base do
16
16
  let(:successful_response) do
17
17
  Typhoeus::Response.new(
18
18
  code: 200,
19
- body: { status: 200, message: "Success"}.to_json,
19
+ body: { status: 200, message: "Success" }.to_json,
20
20
  headers: { "Content-Type" => "application/json"} )
21
21
  end
22
22
 
@@ -277,7 +277,7 @@ describe Px::Service::Client::Base do
277
277
  end.run
278
278
 
279
279
  resp
280
- end.value!['code']).to eq(200)
280
+ end.value!.response_code).to be(200)
281
281
  end
282
282
  end
283
283
 
@@ -18,12 +18,21 @@ describe Px::Service::Client::Caching do
18
18
  }
19
19
 
20
20
  let (:url) { "http://search/foo?bar=baz" }
21
- let(:response) { { "response" => ["foo", "bar"], "status" => 200 } }
22
- let(:entry) { Px::Service::Client::Caching::CacheEntry.new(dalli, url, 'general', response) }
21
+ let(:multi) { Px::Service::Client::Multiplexer.new }
22
+ let(:request) { Typhoeus::Request.new(url, method: :get) }
23
+ let(:future) { Px::Service::Client::RetriableResponseFuture.new(request) }
24
+ let(:response) do
25
+ Typhoeus::Response.new(
26
+ code: 200,
27
+ body: { status: 200, message: "Success" }.to_json,
28
+ headers: { "Content-Type" => "application/json"} )
29
+ end
30
+ let(:entry) { Px::Service::Client::Caching::CacheEntry.new(dalli, url, 'general', response.options) }
23
31
  let(:strategy) { :none }
24
32
 
25
33
  before :each do
26
34
  dalli.flush_all
35
+ Typhoeus.stub(url).and_return(response)
27
36
  end
28
37
 
29
38
  shared_examples_for "a successful request" do
@@ -38,8 +47,13 @@ describe Px::Service::Client::Caching do
38
47
 
39
48
  it "should return the block's return value" do
40
49
  expect(subject.cache_request(url, strategy: strategy) do
41
- response
42
- end.value!).to eq(response)
50
+ resp = nil
51
+ multi.context do
52
+ resp = multi.do(future)
53
+ end.run
54
+
55
+ resp
56
+ end.value!.options).to eq(response.options)
43
57
  end
44
58
  end
45
59
 
@@ -68,6 +82,22 @@ describe Px::Service::Client::Caching do
68
82
  context "when not caching" do
69
83
  it_behaves_like "a successful request"
70
84
  it_behaves_like "a failed uncacheable request"
85
+
86
+ context 'when cache client is not set' do
87
+ before :each do
88
+ subject.class.caching do |config|
89
+ config.cache_client = nil
90
+ end
91
+ end
92
+
93
+ it 'does not raise an exception' do
94
+ expect {
95
+ subject.cache_request(url, strategy: strategy) do
96
+ nil
97
+ end
98
+ }.to_not raise_error
99
+ end
100
+ end
71
101
  end
72
102
 
73
103
  context "when caching as a last resort" do
@@ -77,29 +107,47 @@ describe Px::Service::Client::Caching do
77
107
  it_behaves_like "a failed uncacheable request"
78
108
 
79
109
  context "when there is a cached response" do
80
- before :each do
81
- Px::Service::Client::Caching::CacheEntry.stub(:fetch).and_return(entry)
82
- end
110
+ context 'when cache client is not set' do
111
+ before :each do
112
+ subject.class.caching do |config|
113
+ config.cache_client = nil
114
+ end
115
+ end
83
116
 
84
- it "returns the cached response on failure" do
85
- expect(subject.cache_request(url, strategy: strategy) do
86
- Px::Service::Client::Future.new { raise Px::Service::ServiceError.new("Error", 500) }
87
- end.value!).to eq(response)
117
+ it 'raises an argument exception' do
118
+ expect {
119
+ subject.cache_request(url, strategy: strategy) do
120
+ Px::Service::Client::Future.new { raise Px::Service::ServiceError.new("Error", 500) }
121
+ end.value!
122
+ }.to raise_error(ArgumentError)
123
+ end
88
124
  end
89
125
 
90
- it "does not returns the cached response on request error" do
91
- expect {
92
- subject.cache_request(url, strategy: strategy) do
93
- Px::Service::Client::Future.new { raise Px::Service::ServiceRequestError.new("Error", 404) }
94
- end.value!
95
- }.to raise_error(Px::Service::ServiceRequestError)
96
- end
126
+ context 'when the cache client is set' do
127
+ before :each do
128
+ Px::Service::Client::Caching::CacheEntry.stub(:fetch).and_return(entry)
129
+ end
97
130
 
98
- it "touches the cache entry on failure" do
99
- expect(dalli).to receive(:touch).with(a_kind_of(String), a_kind_of(Fixnum))
131
+ it "returns the cached response on failure" do
132
+ expect(subject.cache_request(url, strategy: strategy) do
133
+ Px::Service::Client::Future.new { raise Px::Service::ServiceError.new("Error", 500) }
134
+ end.value!.options).to eq(response.options.stringify_keys)
135
+ end
100
136
 
101
- subject.cache_request(url, strategy: strategy) do
102
- Px::Service::Client::Future.new { raise Px::Service::ServiceError.new("Error", 500) }
137
+ it "does not returns the cached response on request error" do
138
+ expect {
139
+ subject.cache_request(url, strategy: strategy) do
140
+ Px::Service::Client::Future.new { raise Px::Service::ServiceRequestError.new("Error", 404) }
141
+ end.value!
142
+ }.to raise_error(Px::Service::ServiceRequestError)
143
+ end
144
+
145
+ it "touches the cache entry on failure" do
146
+ expect(dalli).to receive(:touch).with(a_kind_of(String), a_kind_of(Fixnum))
147
+
148
+ subject.cache_request(url, strategy: strategy) do
149
+ Px::Service::Client::Future.new { raise Px::Service::ServiceError.new("Error", 500) }
150
+ end
103
151
  end
104
152
  end
105
153
  end
@@ -114,24 +162,42 @@ describe Px::Service::Client::Caching do
114
162
  it_behaves_like "a failed uncacheable request"
115
163
 
116
164
  context "when there is a cached response" do
117
- before :each do
118
- Px::Service::Client::Caching::CacheEntry.stub(:fetch).and_return(entry)
119
- entry.expires_at = DateTime.now + 1.day
165
+ context 'when cache client is not set' do
166
+ before :each do
167
+ subject.class.caching do |config|
168
+ config.cache_client = nil
169
+ end
170
+ end
171
+
172
+ it 'raises an argument exception' do
173
+ expect {
174
+ subject.cache_request(url, strategy: strategy) do
175
+ nil
176
+ end.value!
177
+ }.to raise_error(ArgumentError)
178
+ end
120
179
  end
121
180
 
122
- it "does not invoke the block" do
123
- called = false
124
- subject.cache_request(url, strategy: strategy) do
125
- called = true
181
+ context 'when the cache client is set' do
182
+ before :each do
183
+ Px::Service::Client::Caching::CacheEntry.stub(:fetch).and_return(entry)
184
+ entry.expires_at = DateTime.now + 1.day
126
185
  end
127
186
 
128
- expect(called).to be_falsey
129
- end
187
+ it "does not invoke the block" do
188
+ called = false
189
+ subject.cache_request(url, strategy: strategy) do
190
+ called = true
191
+ end
130
192
 
131
- it "returns the response" do
132
- expect(subject.cache_request(url, strategy: strategy) do
133
- nil
134
- end.value!).to eq(response)
193
+ expect(called).to be_falsey
194
+ end
195
+
196
+ it "returns the response" do
197
+ expect(subject.cache_request(url, strategy: strategy) do
198
+ nil
199
+ end.value!.options).to eq(response.options.stringify_keys)
200
+ end
135
201
  end
136
202
  end
137
203
 
@@ -141,7 +207,12 @@ describe Px::Service::Client::Caching do
141
207
  entry.expires_at = DateTime.now - 1.day
142
208
  end
143
209
 
144
- let (:response) { { "value" => "response" } }
210
+ let(:response) do
211
+ Typhoeus::Response.new(
212
+ code: 200,
213
+ body: { status: 200, message: "New response" }.to_json,
214
+ headers: { "Content-Type" => "application/json"} )
215
+ end
145
216
 
146
217
  it "invokes the block" do
147
218
  called = false
@@ -153,9 +224,18 @@ describe Px::Service::Client::Caching do
153
224
  end
154
225
 
155
226
  it "returns the new response" do
156
- expect(subject.cache_request(url, strategy: strategy) do
157
- response
158
- end.value!).to eq(response)
227
+ result = subject.cache_request(url, strategy: strategy) do
228
+ resp = nil
229
+ multi.context do
230
+ resp = multi.do(future)
231
+ end.run
232
+
233
+ resp
234
+ end.value!
235
+
236
+ body = JSON.parse(result.body)
237
+
238
+ expect(body[:message]).to eq(JSON.parse(response.body)[:message])
159
239
  end
160
240
 
161
241
  it "updates the cache entry before making the request" do
@@ -166,8 +246,13 @@ describe Px::Service::Client::Caching do
166
246
  called = false
167
247
  expect(subject.cache_request(url, strategy: strategy) do
168
248
  called = true
169
- response
170
- end.value!).to eq(response)
249
+ resp = nil
250
+ multi.context do
251
+ resp = multi.do(future)
252
+ end.run
253
+
254
+ resp
255
+ end.value!.options).to eq(response.options.stringify_keys)
171
256
 
172
257
  expect(called).to be_falsey
173
258
 
@@ -177,18 +262,23 @@ describe Px::Service::Client::Caching do
177
262
 
178
263
  it "caches the new response" do
179
264
  subject.cache_request(url, strategy: strategy) do
180
- response
265
+ resp = nil
266
+ multi.context do
267
+ resp = multi.do(future)
268
+ end.run
269
+
270
+ resp
181
271
  end
182
272
 
183
273
  expect(subject.cache_request(url, strategy: strategy) do
184
274
  nil
185
- end.value).to eq(response)
275
+ end.value.options).to eq(response.options.stringify_keys)
186
276
  end
187
277
 
188
278
  it "returns the cached response on failure" do
189
279
  expect(subject.cache_request(url, strategy: strategy) do
190
280
  Px::Service::Client::Future.new { raise Px::Service::ServiceError.new("Error", 500) }
191
- end.value!).to eq(response)
281
+ end.value!.options).to eq(response.options.stringify_keys)
192
282
  end
193
283
 
194
284
  it "does not returns the cached response on request error" do
@@ -2,13 +2,7 @@ require 'spec_helper'
2
2
 
3
3
  describe Px::Service::Client::CircuitBreaker do
4
4
  let(:subject_class) {
5
- Class.new do
6
- def make_request(method, uri, query: nil, headers: nil, body: nil, timeout: 0)
7
- Px::Service::Client::Future.new do
8
- _result
9
- end
10
- end
11
- end.tap do |c|
5
+ Class.new(Px::Service::Client::Base).tap do |c|
12
6
  # Anonymous classes don't have a name. Stub out :name so that things work
13
7
  allow(c).to receive(:name).and_return("CircuitBreaker")
14
8
  c.include(Px::Service::Client::CircuitBreaker)
@@ -36,19 +30,37 @@ describe Px::Service::Client::CircuitBreaker do
36
30
  end
37
31
 
38
32
  describe '#make_request' do
39
- context "when the underlying request method succeeds" do
40
- before :each do
41
- subject_class.send(:define_method, :_result) do
42
- "returned test"
43
- end
44
- end
33
+ let(:url) { "http://test" }
34
+ let(:multi) { Px::Service::Client::Multiplexer.new }
35
+ let(:response) do
36
+ Typhoeus::Response.new(
37
+ code: 200,
38
+ body: { status: 200, message: "Success" }.to_json,
39
+ headers: { "Content-Type" => "application/json"} )
40
+ end
45
41
 
46
- it "returns a Future" do
47
- expect(subject.make_request(:get, "http://test")).to be_a_kind_of(Px::Service::Client::Future)
42
+ let(:request) do
43
+ req = @object.send(:make_request, 'get', url)
44
+
45
+ multi.context do
46
+ multi.do(req)
47
+ end.run
48
+
49
+ req
50
+ end
51
+
52
+ before :each do
53
+ @object = subject
54
+ Typhoeus.stub(url).and_return(response)
55
+ end
56
+
57
+ context "when the underlying request method succeeds" do
58
+ it "returns a RetriableResponseFuture" do
59
+ expect(subject.send(:make_request, 'get', url)).to be_a_kind_of(Px::Service::Client::RetriableResponseFuture)
48
60
  end
49
61
 
50
62
  it "returns the return value" do
51
- expect(subject.make_request(:get, "http://test").value!).to eq("returned test")
63
+ expect(request.value!).to eq(response)
52
64
  end
53
65
 
54
66
  context "when the breaker is open" do
@@ -62,84 +74,76 @@ describe Px::Service::Client::CircuitBreaker do
62
74
 
63
75
  it "resets the failure count of the breaker" do
64
76
  expect {
65
- subject.make_request(:get, "http://test").value!
77
+ request.value!
66
78
  }.to change{subject.class.circuit_state.failure_count}.to(0)
67
79
  end
68
80
 
69
81
  it "closes the breaker" do
70
82
  expect {
71
- subject.make_request(:get, "http://test").value!
83
+ request.value!
72
84
  }.to change{subject.class.circuit_state.closed?}.from(false).to(true)
73
85
  end
74
86
  end
75
87
  end
76
88
 
77
89
  context "when the wrapped method fails with a ServiceRequestError" do
78
- before :each do
79
- subject_class.send(:define_method, :_result) do
80
- raise Px::Service::ServiceRequestError.new("Error", 404)
81
- end
90
+ let(:response) do
91
+ Typhoeus::Response.new(
92
+ code: 404,
93
+ body: { status: 404, error: "Not Found"}.to_json,
94
+ headers: { "Content-Type" => "application/json"} )
82
95
  end
83
96
 
84
97
  it "raises a ServiceRequestError" do
85
98
  expect {
86
- subject.make_request(:get, "http://test").value!
87
- }.to raise_error(Px::Service::ServiceRequestError, "Error")
99
+ request.value!
100
+ }.to raise_error(Px::Service::ServiceRequestError, "Not Found")
88
101
  end
89
102
 
90
103
  it "does not increment the failure count of the breaker" do
91
104
  expect {
92
- subject.make_request(:get, "http://test").value! rescue nil
105
+ request.value! rescue nil
93
106
  }.not_to change{subject.class.circuit_state.failure_count}
94
107
  end
95
108
  end
96
109
 
97
110
  context "when the wrapped method fails with a ServiceError" do
98
- before :each do
99
- subject_class.send(:define_method, :_result) do
100
- raise Px::Service::ServiceError.new("Error", 500)
101
- end
111
+ let(:response) do
112
+ Typhoeus::Response.new(
113
+ code: 500,
114
+ body: { status: 500, error: "Error"}.to_json,
115
+ headers: { "Content-Type" => "application/json"} )
102
116
  end
103
117
 
104
118
  it "raises a ServiceError" do
105
119
  expect {
106
- subject.make_request(:get, "http://test").value!
120
+ request.value!
107
121
  }.to raise_error(Px::Service::ServiceError, "Error")
108
122
  end
109
123
 
110
124
  it "increments the failure count of the breaker" do
111
125
  expect {
112
- subject.make_request(:get, "http://test").value! rescue nil
126
+ request.value! rescue nil
113
127
  }.to change{subject.class.circuit_state.failure_count}.by(1)
114
128
  end
115
129
  end
116
130
 
117
131
  context "when the circuit is open" do
118
132
  before :each do
119
- subject_class.send(:define_method, :_result) do
120
- "should not be called"
121
- end
122
-
123
133
  subject.circuit_state.trip
124
134
  subject.circuit_state.last_failure_time = Time.now
125
135
  end
126
136
 
127
137
  it "raises a ServiceError" do
128
138
  expect {
129
- subject.make_request(:get, "http://test").value!
139
+ request.value!
130
140
  }.to raise_error(Px::Service::ServiceError)
131
141
  end
132
142
  end
133
143
 
134
144
  context "with multiple classes" do
135
145
  let(:other_class) {
136
- Class.new do
137
- def make_request(method, uri, query: nil, headers: nil, body: nil, timeout: 0)
138
- Px::Service::Client::Future.new do
139
- "result"
140
- end
141
- end
142
- end.tap do |c|
146
+ Class.new(Px::Service::Client::Base).tap do |c|
143
147
  # Anonymous classes don't have a name. Stub out :name so that things work
144
148
  allow(c).to receive(:name).and_return("OtherCircuitBreaker")
145
149
  c.include(Px::Service::Client::CircuitBreaker)
@@ -148,12 +152,6 @@ describe Px::Service::Client::CircuitBreaker do
148
152
 
149
153
  let(:other) { other_class.new }
150
154
 
151
- before :each do
152
- subject_class.send(:define_method, :_result) do
153
- "should not be called"
154
- end
155
- end
156
-
157
155
  context "when the breaker opens on the first instance" do
158
156
  before :each do
159
157
  subject.circuit_state.trip
@@ -162,12 +160,13 @@ describe Px::Service::Client::CircuitBreaker do
162
160
 
163
161
  it "raises a ServiceError on the first instance" do
164
162
  expect {
165
- subject.make_request(:get, "http://test").value!
163
+ request.value!
166
164
  }.to raise_error(Px::Service::ServiceError)
167
165
  end
168
166
 
169
167
  it "does not raise a ServiceError on the second instance" do
170
- expect(other.make_request(:get, "http://test").value!).to eq("result")
168
+ @object = other
169
+ expect(request.value!).to eq(response)
171
170
  end
172
171
  end
173
172
  end
@@ -175,12 +174,6 @@ describe Px::Service::Client::CircuitBreaker do
175
174
  context "with multiple instances of the same class" do
176
175
  let(:other) { subject_class.new }
177
176
 
178
- before :each do
179
- subject_class.send(:define_method, :_result) do
180
- "should not be called"
181
- end
182
- end
183
-
184
177
  context "when the breaker opens on the first instance" do
185
178
  before :each do
186
179
  subject.circuit_state.trip
@@ -189,13 +182,14 @@ describe Px::Service::Client::CircuitBreaker do
189
182
 
190
183
  it "raises a ServiceError on the first instance" do
191
184
  expect {
192
- subject.make_request(:get, "http://test").value!
185
+ request.value!
193
186
  }.to raise_error(Px::Service::ServiceError)
194
187
  end
195
188
 
196
189
  it "raises a ServiceError on the second instance" do
190
+ @object = other
197
191
  expect {
198
- other.make_request(:get, "http://test").value!
192
+ request.value!
199
193
  }.to raise_error(Px::Service::ServiceError)
200
194
  end
201
195
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: px-service-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chris Micacchi
@@ -198,14 +198,14 @@ dependencies:
198
198
  requirements:
199
199
  - - "~>"
200
200
  - !ruby/object:Gem::Version
201
- version: '2.14'
201
+ version: '2.99'
202
202
  type: :development
203
203
  prerelease: false
204
204
  version_requirements: !ruby/object:Gem::Requirement
205
205
  requirements:
206
206
  - - "~>"
207
207
  - !ruby/object:Gem::Version
208
- version: '2.14'
208
+ version: '2.99'
209
209
  - !ruby/object:Gem::Dependency
210
210
  name: timecop
211
211
  requirement: !ruby/object:Gem::Requirement
@@ -244,6 +244,7 @@ files:
244
244
  - lib/px/service/client/caching/log_subscriber.rb
245
245
  - lib/px/service/client/caching/railtie.rb
246
246
  - lib/px/service/client/circuit_breaker.rb
247
+ - lib/px/service/client/circuit_breaker_retriable_response_future.rb
247
248
  - lib/px/service/client/future.rb
248
249
  - lib/px/service/client/list_response.rb
249
250
  - lib/px/service/client/multiplexer.rb