px-service-client 1.0.1 → 1.0.4

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: af7a7e8bbce817e251279c1a31fe9737e05d7d2d
4
- data.tar.gz: deef44499ba41a91b75c2d1efbf4a55568a5fa62
3
+ metadata.gz: 31469d2437619f1f3afec21f42201aa2054ade9b
4
+ data.tar.gz: 55491249709bd72ab3e90fa9648cb72dee46880b
5
5
  SHA512:
6
- metadata.gz: 7caf92bd8294aafe32fa12de6c5328d51960cee2c19c93ea42dd48a335778d13b83fa34b44e05a58a879f5720deb0954b0c6b5cfe6a84c1a8496752b46cfc60a
7
- data.tar.gz: cc7781a37f252033c1e585d31e6eb125fc1c5bda0f15c7396a3de44ca767cfc91aff3fa6f2e280d48088220c3df459022464a5db30c6d98c4df0317ce7d4408e
6
+ metadata.gz: bb3b61faa7e47e838f45db03731effe82ef5c92ca109878bd592ab80869ab8a00706c611dd586d253078cd9c01b48ae28dda04fd650c71a1cfdefe2dafd8419d
7
+ data.tar.gz: b86dff5d8658f923ae59494edc8314dd2eb371b2e4757ace908598a106c084f4c02ffa878cd05a882bbcc4056df4cbd7f93335d03f67de75b25aaef6415c9bfd
data/README.md CHANGED
@@ -24,8 +24,8 @@ Then use it:
24
24
  require 'px-service-client'
25
25
 
26
26
  class MyClient
27
- include Px::Service::Client::::Caching
28
- include Px::Service::Client::::CircuitBreaker
27
+ include Px::Service::Client::Caching
28
+ include Px::Service::Client::CircuitBreaker
29
29
  end
30
30
 
31
31
  ```
@@ -37,10 +37,10 @@ This gem includes several common features used in 500px service client libraries
37
37
 
38
38
  The features are:
39
39
 
40
- #### Px::Service::Client::::Caching
40
+ #### Px::Service::Client::Caching
41
41
 
42
42
  ```ruby
43
- include Px::Service::Client::::Caching
43
+ include Px::Service::Client::Caching
44
44
 
45
45
  self.cache_client = Dalli::Client.new(...)
46
46
  self.cache_logger = Logger.new(STDOUT) # or Rails.logger, for example
@@ -55,13 +55,9 @@ to be refreshed probabilistically (rather than on every request).
55
55
  *first-resort* means that the cached value is always used, if present. Requests to the service are only made
56
56
  when the cached value is close to expiry.
57
57
 
58
- #### Px::Service::Client::::CircuitBreaker
58
+ #### Px::Service::Client::CircuitBreaker
59
59
 
60
60
  ```ruby
61
- def call_remote_service() ...
62
-
63
- circuit_method :call_remote_service
64
-
65
61
  # Optional
66
62
  circuit_handler do |handler|
67
63
  handler.logger = Logger.new(STDOUT)
@@ -72,25 +68,21 @@ circuit_handler do |handler|
72
68
  end
73
69
  ```
74
70
 
75
- Provides a circuit breaker on the class, and turns the class into a singleton. Each method named using
76
- `circuit_method` will be wrapped in a circuit breaker that will raise a `Px::Service::ServiceError` if the breaker
77
- is open. The circuit will open on any exception from the wrapped method, or if the wrapped method
78
- runs for longer than the `invocation_timeout`.
71
+ Adds a circuit breaker to the client. The circuit will open on any exception from the wrapped method, or if the request runs for longer than the `invocation_timeout`.
79
72
 
80
- Note that `Px::Service::ServiceRequestError` exceptions do NOT trip the breaker, as these exceptions indicate an error
81
- on the caller's part (e.g. an HTTP 4xx error).
73
+ Note that `Px::Service::ServiceRequestError` exceptions do NOT trip the breaker, as these exceptions indicate an error on the caller's part (e.g. an HTTP 4xx error).
82
74
 
83
- The class is made a singleton using the standard `Singleton` module. Access to the class's methods should be done
84
- using its `instance` class method (calls to `new` will fail).
75
+ Every instance of the class that includes the `CircuitBreaker` concern will share the same circuit state. You should therefore include `Px::Service::Client::CircuitBreaker` in the most-derived class that subclasses
76
+ `Px::Service::Client::Base`
85
77
 
86
78
  This module is based on (and uses) the [Circuit Breaker](https://github.com/wsargent/circuit_breaker) gem by Will Sargent.
87
79
 
88
- #### Px::Service::Client::::ListResponse
80
+ #### Px::Service::Client::ListResponse
89
81
 
90
82
  ```ruby
91
83
  def get_something(page, page_size)
92
84
  response = JSON.parse(http_get("http://some/url?p=#{page}&l=#{page_size}"))
93
- return Px::Service::Client::::ListResponse(page_size, response, "items")
85
+ return Px::Service::Client::ListResponse(page_size, response, "items")
94
86
  end
95
87
 
96
88
  ```
@@ -1,7 +1,6 @@
1
1
  module Px::Service::Client
2
2
  class Base
3
3
  include Px::Service::Client::Caching
4
- include Px::Service::Client::CircuitBreaker
5
4
  cattr_accessor :logger
6
5
 
7
6
  private
@@ -18,13 +17,16 @@ module Px::Service::Client
18
17
  end
19
18
  end
20
19
 
21
- def make_request(method, uri, query: nil, headers: nil, body: nil)
20
+ ##
21
+ # Make the request
22
+ def make_request(method, uri, query: nil, headers: nil, body: nil, timeout: 0)
22
23
  req = Typhoeus::Request.new(
23
24
  uri,
24
25
  method: method,
25
26
  params: query,
26
27
  body: body,
27
- headers: headers)
28
+ headers: headers,
29
+ timeout: timeout)
28
30
 
29
31
  start_time = Time.now
30
32
  logger.debug "Making request #{method.to_s.upcase} #{uri}" if logger
@@ -36,6 +38,5 @@ module Px::Service::Client
36
38
 
37
39
  RetriableResponseFuture.new(req)
38
40
  end
39
-
40
41
  end
41
42
  end
@@ -3,10 +3,9 @@ require 'circuit_breaker'
3
3
  module Px::Service::Client
4
4
  module CircuitBreaker
5
5
  extend ActiveSupport::Concern
6
+ include ::CircuitBreaker
6
7
 
7
8
  included do
8
- include ::CircuitBreaker
9
-
10
9
  # Default circuit breaker configuration. Can be overridden
11
10
  circuit_handler do |handler|
12
11
  handler.failure_threshold = 5
@@ -15,33 +14,49 @@ module Px::Service::Client
15
14
  handler.excluded_exceptions = [Px::Service::ServiceRequestError]
16
15
  end
17
16
 
18
- class <<self
19
- alias_method_chain :circuit_method, :exceptions
17
+ cattr_accessor :circuit_state do
18
+ ::CircuitBreaker::CircuitState.new
20
19
  end
21
- end
22
20
 
21
+ alias_method_chain :make_request, :breaker
22
+ end
23
23
 
24
- module ClassMethods
25
- ##
26
- # Takes a splat of method names, and wraps them with the circuit_handler.
27
- # Overrides the circuit_method provided by ::CircuitBreaker
28
- def circuit_method_with_exceptions(*methods)
29
- circuit_handler = self.circuit_handler
30
-
31
- methods.each do |meth|
32
- m = instance_method(meth)
33
- define_method(meth) do |*args|
34
- begin
35
- circuit_handler.handle(self.circuit_state, m.bind(self), *args)
36
- rescue Px::Service::ServiceError, Px::Service::ServiceRequestError => ex
37
- raise ex
38
- rescue StandardError => ex
39
- # Wrap other exceptions, includes CircuitBreaker::CircuitBrokenException
40
- raise Px::Service::ServiceError.new(ex.message, 503), ex, ex.backtrace
41
- end
24
+ ##
25
+ # Make the request, respecting the circuit breaker, if configured
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
42
38
  end
43
39
  end
40
+
41
+ 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
57
+ end
44
58
  end
45
59
  end
60
+
46
61
  end
47
62
  end
@@ -56,6 +56,17 @@ module Px::Service::Client
56
56
  end
57
57
  end
58
58
 
59
+ def value!
60
+ if @completed
61
+ result = @value
62
+ else
63
+ result = wait_for_value(nil)
64
+ end
65
+ raise result if result.kind_of?(Exception)
66
+
67
+ result
68
+ end
69
+
59
70
  def completed?
60
71
  @completed
61
72
  end
@@ -1,7 +1,7 @@
1
1
  module Px
2
2
  module Service
3
3
  module Client
4
- VERSION = "1.0.1"
4
+ VERSION = "1.0.4"
5
5
  end
6
6
  end
7
7
  end
@@ -26,6 +26,7 @@ Gem::Specification.new do |spec|
26
26
  spec.add_development_dependency "bundler", "~> 1.6"
27
27
  spec.add_development_dependency "rake"
28
28
  spec.add_development_dependency "webmock"
29
+ spec.add_development_dependency "pry"
29
30
  spec.add_development_dependency "pry-byebug"
30
31
  spec.add_development_dependency "vcr"
31
32
  spec.add_development_dependency "guard"
@@ -1,11 +1,17 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe Px::Service::Client::CircuitBreaker do
4
-
5
4
  let(:subject_class) {
6
- Class.new.include(Px::Service::Client::CircuitBreaker).tap do |c|
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|
7
12
  # Anonymous classes don't have a name. Stub out :name so that things work
8
13
  allow(c).to receive(:name).and_return("CircuitBreaker")
14
+ c.include(Px::Service::Client::CircuitBreaker)
9
15
  end
10
16
  }
11
17
 
@@ -29,85 +35,171 @@ describe Px::Service::Client::CircuitBreaker do
29
35
  end
30
36
  end
31
37
 
32
- describe '#circuit_method' do
33
- context "when the wrapped method succeeds" do
38
+ describe '#make_request' do
39
+ context "when the underlying request method succeeds" do
34
40
  before :each do
35
- subject_class.send(:define_method, :test_method) do |arg|
36
- "returned #{arg}"
41
+ subject_class.send(:define_method, :_result) do
42
+ "returned test"
37
43
  end
44
+ end
38
45
 
39
- subject_class.circuit_method(:test_method)
46
+ it "returns a Future" do
47
+ expect(subject.make_request(:get, "http://test")).to be_a_kind_of(Px::Service::Client::Future)
40
48
  end
41
49
 
42
50
  it "returns the return value" do
43
- expect(subject.test_method("test")).to eq("returned test")
51
+ expect(subject.make_request(:get, "http://test").value!).to eq("returned test")
52
+ end
53
+
54
+ context "when the breaker is open" do
55
+ before :each do
56
+ allow(subject_class.circuit_handler).to receive(:is_timeout_exceeded).and_return(true)
57
+
58
+ subject.circuit_state.trip
59
+ subject.circuit_state.last_failure_time = Time.now
60
+ subject.circuit_state.failure_count = 5
61
+ end
62
+
63
+ it "resets the failure count of the breaker" do
64
+ expect {
65
+ subject.make_request(:get, "http://test").value!
66
+ }.to change{subject.class.circuit_state.failure_count}.to(0)
67
+ end
68
+
69
+ it "closes the breaker" do
70
+ expect {
71
+ subject.make_request(:get, "http://test").value!
72
+ }.to change{subject.class.circuit_state.closed?}.from(false).to(true)
73
+ end
44
74
  end
45
75
  end
46
76
 
47
77
  context "when the wrapped method fails with a ServiceRequestError" do
48
78
  before :each do
49
- subject_class.send(:define_method, :test_method) do |arg|
79
+ subject_class.send(:define_method, :_result) do
50
80
  raise Px::Service::ServiceRequestError.new("Error", 404)
51
81
  end
52
-
53
- subject_class.circuit_method(:test_method)
54
82
  end
55
83
 
56
84
  it "raises a ServiceRequestError" do
57
- expect{
58
- subject.test_method("test")
85
+ expect {
86
+ subject.make_request(:get, "http://test").value!
59
87
  }.to raise_error(Px::Service::ServiceRequestError, "Error")
60
88
  end
89
+
90
+ it "does not increment the failure count of the breaker" do
91
+ expect {
92
+ subject.make_request(:get, "http://test").value! rescue nil
93
+ }.not_to change{subject.class.circuit_state.failure_count}
94
+ end
61
95
  end
62
96
 
63
97
  context "when the wrapped method fails with a ServiceError" do
64
98
  before :each do
65
- subject_class.send(:define_method, :test_method) do |arg|
99
+ subject_class.send(:define_method, :_result) do
66
100
  raise Px::Service::ServiceError.new("Error", 500)
67
101
  end
68
-
69
- subject_class.circuit_method(:test_method)
70
102
  end
71
103
 
72
104
  it "raises a ServiceError" do
73
- expect{
74
- subject.test_method("test")
105
+ expect {
106
+ subject.make_request(:get, "http://test").value!
75
107
  }.to raise_error(Px::Service::ServiceError, "Error")
76
108
  end
109
+
110
+ it "increments the failure count of the breaker" do
111
+ expect {
112
+ subject.make_request(:get, "http://test").value! rescue nil
113
+ }.to change{subject.class.circuit_state.failure_count}.by(1)
114
+ end
77
115
  end
78
116
 
79
- context "when the wrapped method fails with another exception" do
117
+ context "when the circuit is open" do
80
118
  before :each do
81
- subject_class.send(:define_method, :test_method) do |arg|
82
- this_is_not_a_method # Raises NoMethodError
119
+ subject_class.send(:define_method, :_result) do
120
+ "should not be called"
83
121
  end
84
122
 
85
- subject_class.circuit_method(:test_method)
123
+ subject.circuit_state.trip
124
+ subject.circuit_state.last_failure_time = Time.now
86
125
  end
87
126
 
88
127
  it "raises a ServiceError" do
89
- expect{
90
- subject.test_method("test")
128
+ expect {
129
+ subject.make_request(:get, "http://test").value!
91
130
  }.to raise_error(Px::Service::ServiceError)
92
131
  end
93
132
  end
94
133
 
95
- context "when the circuit is open" do
134
+ context "with multiple classes" do
135
+ 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|
143
+ # Anonymous classes don't have a name. Stub out :name so that things work
144
+ allow(c).to receive(:name).and_return("OtherCircuitBreaker")
145
+ c.include(Px::Service::Client::CircuitBreaker)
146
+ end
147
+ }
148
+
149
+ let(:other) { other_class.new }
150
+
96
151
  before :each do
97
- subject_class.send(:define_method, :test_method) do |arg|
152
+ subject_class.send(:define_method, :_result) do
98
153
  "should not be called"
99
154
  end
155
+ end
100
156
 
101
- subject_class.circuit_method(:test_method)
157
+ context "when the breaker opens on the first instance" do
158
+ before :each do
159
+ subject.circuit_state.trip
160
+ subject.circuit_state.last_failure_time = Time.now
161
+ end
102
162
 
103
- subject.circuit_state.trip
163
+ it "raises a ServiceError on the first instance" do
164
+ expect {
165
+ subject.make_request(:get, "http://test").value!
166
+ }.to raise_error(Px::Service::ServiceError)
167
+ end
168
+
169
+ it "does not raise a ServiceError on the second instance" do
170
+ expect(other.make_request(:get, "http://test").value!).to eq("result")
171
+ end
104
172
  end
173
+ end
105
174
 
106
- it "raises a ServiceError" do
107
- expect{
108
- subject.test_method("test")
109
- }.to raise_error(Px::Service::ServiceError)
175
+ context "with multiple instances of the same class" do
176
+ let(:other) { subject_class.new }
177
+
178
+ before :each do
179
+ subject_class.send(:define_method, :_result) do
180
+ "should not be called"
181
+ end
182
+ end
183
+
184
+ context "when the breaker opens on the first instance" do
185
+ before :each do
186
+ subject.circuit_state.trip
187
+ subject.circuit_state.last_failure_time = Time.now
188
+ end
189
+
190
+ it "raises a ServiceError on the first instance" do
191
+ expect {
192
+ subject.make_request(:get, "http://test").value!
193
+ }.to raise_error(Px::Service::ServiceError)
194
+ end
195
+
196
+ it "raises a ServiceError on the second instance" do
197
+ expect {
198
+ other.make_request(:get, "http://test").value!
199
+ }.to raise_error(Px::Service::ServiceError)
200
+ end
110
201
  end
111
202
  end
203
+
112
204
  end
113
205
  end
@@ -46,7 +46,7 @@ describe Px::Service::Client::Future do
46
46
  it "does not call the method on the value" do
47
47
  called = false
48
48
  Fiber.new do
49
- subject.size
49
+ subject.value
50
50
  called = true
51
51
  end.resume
52
52
 
@@ -86,6 +86,53 @@ describe Px::Service::Client::Future do
86
86
  end
87
87
  end
88
88
 
89
+ describe '#value!' do
90
+ context "when the future is not complete" do
91
+ it "does not call the method on the value" do
92
+ called = false
93
+ Fiber.new do
94
+ subject.value!
95
+ called = true
96
+ end.resume
97
+
98
+ expect(called).to eq(false)
99
+ end
100
+ end
101
+
102
+ context "when the future is already complete" do
103
+ it "returns the value" do
104
+ subject.complete(value)
105
+ expect(subject.value!).to eq(value)
106
+ end
107
+ end
108
+
109
+ context "when the value is an exception" do
110
+ it "returns the exception" do
111
+ Fiber.new do
112
+ expect {
113
+ subject.value!
114
+ }.to raise_error(ArgumentError)
115
+ end.resume
116
+
117
+ Fiber.new do
118
+ subject.complete(ArgumentError.new("Error"))
119
+ end.resume
120
+ end
121
+ end
122
+
123
+ context "when the method returns a value" do
124
+ it "returns the value" do
125
+ Fiber.new do
126
+ expect(subject.value!).to eq(value)
127
+ end.resume
128
+
129
+ Fiber.new do
130
+ subject.complete(value)
131
+ end.resume
132
+ end
133
+ end
134
+ end
135
+
89
136
  describe '#method_missing' do
90
137
  context "when the future is already complete" do
91
138
  context "when the method raised an exception" do
data/spec/spec_helper.rb CHANGED
@@ -3,6 +3,7 @@ Bundler.setup
3
3
 
4
4
  require 'px/service/client'
5
5
  require 'timecop'
6
+ require 'pry'
6
7
  require 'vcr'
7
8
 
8
9
  ##
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: px-service-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chris Micacchi
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-12-08 00:00:00.000000000 Z
11
+ date: 2015-08-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: will_paginate
@@ -122,6 +122,20 @@ dependencies:
122
122
  - - ">="
123
123
  - !ruby/object:Gem::Version
124
124
  version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: pry
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
125
139
  - !ruby/object:Gem::Dependency
126
140
  name: pry-byebug
127
141
  requirement: !ruby/object:Gem::Requirement
@@ -269,7 +283,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
269
283
  version: '0'
270
284
  requirements: []
271
285
  rubyforge_project:
272
- rubygems_version: 2.2.2
286
+ rubygems_version: 2.4.5
273
287
  signing_key:
274
288
  specification_version: 4
275
289
  summary: Common service client behaviours for Ruby applications