px-service-client 1.1.0 → 1.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: 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