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