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 +4 -4
- data/README.md +26 -2
- data/lib/px/service/client.rb +1 -0
- data/lib/px/service/client/caching.rb +16 -23
- data/lib/px/service/client/caching/cache_entry.rb +6 -0
- data/lib/px/service/client/circuit_breaker.rb +30 -28
- data/lib/px/service/client/circuit_breaker_retriable_response_future.rb +13 -0
- data/lib/px/service/client/multiplexer.rb +1 -1
- data/lib/px/service/client/version.rb +1 -1
- data/px-service-client.gemspec +1 -1
- data/spec/px/service/client/base_spec.rb +2 -2
- data/spec/px/service/client/caching/caching_spec.rb +134 -44
- data/spec/px/service/client/circuit_breaker_spec.rb +53 -59
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 260462f7c8c782f9a5a1b7a0996431e426f01f39
|
4
|
+
data.tar.gz: 31ffc0fff0138c3d8519d523da451b23e765de0c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
46
|
-
|
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
|
data/lib/px/service/client.rb
CHANGED
@@ -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
|
-
|
75
|
-
|
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
|
-
|
126
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
28
|
-
|
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
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
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
|
|
data/px-service-client.gemspec
CHANGED
@@ -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.
|
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
|
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(:
|
22
|
-
let(:
|
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
|
-
|
42
|
-
|
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
|
-
|
81
|
-
|
82
|
-
|
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
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
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
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
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
|
-
|
99
|
-
|
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
|
-
|
102
|
-
|
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
|
-
|
118
|
-
|
119
|
-
|
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
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
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
|
-
|
129
|
-
|
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
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
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
|
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
|
-
|
157
|
-
|
158
|
-
|
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
|
-
|
170
|
-
|
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
|
-
|
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
|
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
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
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
|
-
|
47
|
-
|
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(
|
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
|
-
|
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
|
-
|
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
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
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
|
-
|
87
|
-
}.to raise_error(Px::Service::ServiceRequestError, "
|
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
|
-
|
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
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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.
|
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.
|
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.
|
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
|