px-service-client 1.0.1 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 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