px-service-client 1.0.1
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 +7 -0
- data/.gitignore +22 -0
- data/.rspec +2 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/Gemfile +4 -0
- data/Guardfile +19 -0
- data/LICENSE.txt +22 -0
- data/README.md +140 -0
- data/Rakefile +9 -0
- data/lib/px/service/client/base.rb +41 -0
- data/lib/px/service/client/caching/cache_entry.rb +95 -0
- data/lib/px/service/client/caching/log_subscriber.rb +23 -0
- data/lib/px/service/client/caching/railtie.rb +11 -0
- data/lib/px/service/client/caching.rb +112 -0
- data/lib/px/service/client/circuit_breaker.rb +47 -0
- data/lib/px/service/client/future.rb +91 -0
- data/lib/px/service/client/list_response.rb +80 -0
- data/lib/px/service/client/multiplexer.rb +34 -0
- data/lib/px/service/client/retriable_response_future.rb +98 -0
- data/lib/px/service/client/version.rb +7 -0
- data/lib/px/service/client.rb +19 -0
- data/lib/px/service/errors.rb +28 -0
- data/lib/px-service-client.rb +1 -0
- data/px-service-client.gemspec +35 -0
- data/spec/px/service/client/base_spec.rb +49 -0
- data/spec/px/service/client/caching/caching_spec.rb +209 -0
- data/spec/px/service/client/circuit_breaker_spec.rb +113 -0
- data/spec/px/service/client/future_spec.rb +182 -0
- data/spec/px/service/client/list_response_spec.rb +118 -0
- data/spec/px/service/client/multiplexer_spec.rb +63 -0
- data/spec/px/service/client/retriable_response_future_spec.rb +99 -0
- data/spec/spec_helper.rb +25 -0
- data/spec/vcr/Px_Service_Client_Multiplexer/with_multiple_requests/when_the_requests_depend_on_each_other/runs_the_requests.yml +91 -0
- data/spec/vcr/Px_Service_Client_Multiplexer/with_multiple_requests/when_the_requests_don_t_depend_on_each_other/runs_the_requests.yml +91 -0
- data/spec/vcr/Px_Service_Client_Multiplexer/with_one_request/returns_a_ResponseFuture.yml +47 -0
- data/spec/vcr/Px_Service_Client_Multiplexer/with_one_request/runs_the_requests.yml +47 -0
- metadata +288 -0
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'will_paginate/collection'
|
2
|
+
|
3
|
+
module Px::Service::Client
|
4
|
+
##
|
5
|
+
# This class implements the methods necessary to be compatible with WillPaginate and Enumerable
|
6
|
+
class ListResponse
|
7
|
+
include WillPaginate::CollectionMethods
|
8
|
+
include Enumerable
|
9
|
+
|
10
|
+
attr_reader :response, :per_page
|
11
|
+
|
12
|
+
DEFAULT_PER_PAGE = 20
|
13
|
+
|
14
|
+
def initialize(page_size, response, results_key, options = {})
|
15
|
+
@response = response
|
16
|
+
@results_key = results_key
|
17
|
+
@options = options
|
18
|
+
@per_page = page_size || DEFAULT_PER_PAGE
|
19
|
+
end
|
20
|
+
|
21
|
+
##
|
22
|
+
# Get the current page
|
23
|
+
def current_page
|
24
|
+
response["current_page"]
|
25
|
+
end
|
26
|
+
|
27
|
+
def offset
|
28
|
+
(current_page - 1) * per_page
|
29
|
+
end
|
30
|
+
|
31
|
+
def total_entries
|
32
|
+
response["total_items"]
|
33
|
+
end
|
34
|
+
alias_method :total, :total_entries
|
35
|
+
|
36
|
+
def total_pages
|
37
|
+
response["total_pages"]
|
38
|
+
end
|
39
|
+
|
40
|
+
def results
|
41
|
+
response[@results_key]
|
42
|
+
end
|
43
|
+
|
44
|
+
def raw_results
|
45
|
+
response[@results_key]
|
46
|
+
end
|
47
|
+
|
48
|
+
##
|
49
|
+
# Support Enumerable
|
50
|
+
def each(&block)
|
51
|
+
results.each(&block)
|
52
|
+
end
|
53
|
+
|
54
|
+
##
|
55
|
+
# Allow comparisons with arrays e.g. in Rspec to succeed
|
56
|
+
def ==(other)
|
57
|
+
if other.class == self.class
|
58
|
+
other.results == self.results
|
59
|
+
elsif other.class <= Array
|
60
|
+
other == self.results
|
61
|
+
else
|
62
|
+
false
|
63
|
+
end
|
64
|
+
end
|
65
|
+
alias_method :eql?, :==
|
66
|
+
|
67
|
+
def empty?
|
68
|
+
results.empty?
|
69
|
+
end
|
70
|
+
|
71
|
+
def method_missing(method_name, *arguments, &block)
|
72
|
+
results.send(method_name, *arguments, &block)
|
73
|
+
end
|
74
|
+
|
75
|
+
def respond_to_missing?(method_name, include_private = false)
|
76
|
+
results.respond_to?(method_name, include_private)
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Px::Service::Client
|
2
|
+
class Multiplexer
|
3
|
+
attr_accessor :hydra
|
4
|
+
attr_accessor :states
|
5
|
+
|
6
|
+
def initialize(params = {})
|
7
|
+
self.hydra = Typhoeus::Hydra.new(params)
|
8
|
+
end
|
9
|
+
|
10
|
+
def context
|
11
|
+
Fiber.new{ yield }.resume
|
12
|
+
self
|
13
|
+
end
|
14
|
+
|
15
|
+
##
|
16
|
+
# Queue a request on the multiplexer, with retry
|
17
|
+
def do(request_or_future, retries: RetriableResponseFuture::DEFAULT_RETRIES)
|
18
|
+
response = request_or_future
|
19
|
+
if request_or_future.is_a?(Typhoeus::Request)
|
20
|
+
response = RetriableResponseFuture(request_or_future, retries: retries)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Will automatically queue the request on the hydra
|
24
|
+
response.hydra = hydra
|
25
|
+
response
|
26
|
+
end
|
27
|
+
|
28
|
+
##
|
29
|
+
# Start the multiplexer.
|
30
|
+
def run
|
31
|
+
hydra.run
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# This is based on this code: https://github.com/bitherder/stitch
|
2
|
+
|
3
|
+
require 'fiber'
|
4
|
+
|
5
|
+
module Px::Service::Client
|
6
|
+
class RetriableResponseFuture < Future
|
7
|
+
DEFAULT_RETRIES = 3
|
8
|
+
|
9
|
+
attr_reader :hydra, :request
|
10
|
+
|
11
|
+
def initialize(request = nil, retries: DEFAULT_RETRIES)
|
12
|
+
super()
|
13
|
+
|
14
|
+
@retries = retries
|
15
|
+
self.request = request if request
|
16
|
+
end
|
17
|
+
|
18
|
+
def request=(request)
|
19
|
+
raise ArgumentError.new("A request has already been assigned") if @request
|
20
|
+
|
21
|
+
@request = request
|
22
|
+
self.request.on_complete do |response|
|
23
|
+
result = handle_error_statuses(response)
|
24
|
+
complete(result)
|
25
|
+
end
|
26
|
+
|
27
|
+
configure_auto_retry(request, @retries)
|
28
|
+
|
29
|
+
hydra.queue(request) if hydra
|
30
|
+
end
|
31
|
+
|
32
|
+
def hydra=(hydra)
|
33
|
+
raise ArgumentError.new("A hydra has already been assigned") if @hydra
|
34
|
+
|
35
|
+
@hydra = hydra
|
36
|
+
hydra.queue(request) if request
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
##
|
42
|
+
# Raise appropriate exception on error statuses
|
43
|
+
def handle_error_statuses(response)
|
44
|
+
return response if response.success?
|
45
|
+
|
46
|
+
begin
|
47
|
+
body = parse_error_body(response)
|
48
|
+
|
49
|
+
if response.response_code >= 400 && response.response_code < 499
|
50
|
+
raise Px::Service::ServiceRequestError.new(body, response.response_code)
|
51
|
+
elsif response.response_code >= 500 || response.response_code == 0
|
52
|
+
raise Px::Service::ServiceError.new(body, response.response_code)
|
53
|
+
end
|
54
|
+
rescue Exception => ex
|
55
|
+
return ex
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def parse_error_body(response)
|
60
|
+
if response.headers && response.headers["Content-Type"] =~ %r{application/json}
|
61
|
+
JSON.parse(response.body)["error"] rescue response.body.try(:strip)
|
62
|
+
else
|
63
|
+
response.body.strip
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
|
68
|
+
##
|
69
|
+
# Configures auto-retry on the request
|
70
|
+
def configure_auto_retry(request, retries)
|
71
|
+
return if retries.nil? || retries == 0
|
72
|
+
# To do this, we have to hijack the Typhoeus callback list, as there's
|
73
|
+
# no way to prevent later callbacks from being executed from earlier callbacks
|
74
|
+
old_on_complete = request.on_complete.dup
|
75
|
+
request.on_complete.clear
|
76
|
+
retries_left = retries
|
77
|
+
|
78
|
+
request.on_complete do |response|
|
79
|
+
if !self.completed?
|
80
|
+
# Don't retry on success, client error, or after exhausting our retry count
|
81
|
+
if response.success? ||
|
82
|
+
response.response_code >= 400 && response.response_code <= 499 ||
|
83
|
+
retries_left <= 0
|
84
|
+
|
85
|
+
# Call the old callbacks
|
86
|
+
old_on_complete.map do |callback|
|
87
|
+
response.handled_response = callback.call(response)
|
88
|
+
end
|
89
|
+
else
|
90
|
+
# Retry
|
91
|
+
retries_left -= 1
|
92
|
+
hydra.queue(response.request)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
require 'active_support/core_ext'
|
3
|
+
require 'px/service/errors'
|
4
|
+
|
5
|
+
module Px
|
6
|
+
module Service
|
7
|
+
module Client
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
require "px/service/client/version"
|
13
|
+
require "px/service/client/future"
|
14
|
+
require "px/service/client/caching"
|
15
|
+
require "px/service/client/circuit_breaker"
|
16
|
+
require "px/service/client/list_response"
|
17
|
+
require "px/service/client/base"
|
18
|
+
require "px/service/client/multiplexer"
|
19
|
+
require "px/service/client/retriable_response_future"
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Px
|
2
|
+
module Service
|
3
|
+
##
|
4
|
+
# Any external service should have its exceptions inherit from this class
|
5
|
+
# so that controllers can handle them all nicely with "service is down" pages or whatnot
|
6
|
+
class ServiceBaseError < StandardError
|
7
|
+
attr_accessor :status
|
8
|
+
|
9
|
+
def initialize(message, status)
|
10
|
+
self.status = status
|
11
|
+
super(message)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
##
|
16
|
+
# Indicates something was wrong with the request (ie, not a service failure, but an error on the caller's
|
17
|
+
# part). Corresponds to HTTP status 4xx responses
|
18
|
+
class ServiceRequestError < ServiceBaseError
|
19
|
+
end
|
20
|
+
|
21
|
+
##
|
22
|
+
# Indicates something went wrong during request processing (a service or network error occurred)
|
23
|
+
# Corresponds to HTTP status 5xx responses.
|
24
|
+
# Services should catch other network/transport errors and raise this exception instead.
|
25
|
+
class ServiceError < ServiceBaseError
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'px/service/client'
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'px/service/client/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "px-service-client"
|
8
|
+
spec.version = Px::Service::Client::VERSION
|
9
|
+
spec.summary = %q{Common service client behaviours for Ruby applications}
|
10
|
+
spec.authors = ["Chris Micacchi"]
|
11
|
+
spec.email = ["chris@500px.com"]
|
12
|
+
spec.homepage = ""
|
13
|
+
spec.license = "MIT"
|
14
|
+
|
15
|
+
spec.files = `git ls-files -z`.split("\x0")
|
16
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
17
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
18
|
+
spec.require_paths = ["lib"]
|
19
|
+
|
20
|
+
spec.add_dependency "will_paginate", "~> 3.0"
|
21
|
+
spec.add_dependency "dalli"
|
22
|
+
spec.add_dependency "typhoeus"
|
23
|
+
spec.add_dependency "activesupport"
|
24
|
+
spec.add_dependency "circuit_breaker", "~> 1.1"
|
25
|
+
|
26
|
+
spec.add_development_dependency "bundler", "~> 1.6"
|
27
|
+
spec.add_development_dependency "rake"
|
28
|
+
spec.add_development_dependency "webmock"
|
29
|
+
spec.add_development_dependency "pry-byebug"
|
30
|
+
spec.add_development_dependency "vcr"
|
31
|
+
spec.add_development_dependency "guard"
|
32
|
+
spec.add_development_dependency "guard-rspec"
|
33
|
+
spec.add_development_dependency "rspec", "~> 2.14"
|
34
|
+
spec.add_development_dependency "timecop", "~> 0.5"
|
35
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Px::Service::Client::Base do
|
4
|
+
subject { Px::Service::Client::Base.send(:new) }
|
5
|
+
let(:response) do
|
6
|
+
Typhoeus::Response.new(
|
7
|
+
code: 200,
|
8
|
+
body: { status: 200, message: "Success"}.to_json,
|
9
|
+
headers: { "Content-Type" => "application/json"} )
|
10
|
+
end
|
11
|
+
|
12
|
+
describe '#make_request' do
|
13
|
+
let(:url) { 'http://localhost:3000/path' }
|
14
|
+
|
15
|
+
it "returns a future response" do
|
16
|
+
expect(subject.send(:make_request, 'get', url)).to be_a(Px::Service::Client::Future)
|
17
|
+
end
|
18
|
+
|
19
|
+
context "with a header" do
|
20
|
+
let(:expected_headers) {
|
21
|
+
{
|
22
|
+
"Cookie" => "_hpx1=cookie",
|
23
|
+
}
|
24
|
+
}
|
25
|
+
|
26
|
+
let(:resp) { subject.send(:make_request, 'get', url, headers: expected_headers) }
|
27
|
+
let(:headers) { resp.request.options[:headers] }
|
28
|
+
|
29
|
+
it "sets the expected header" do
|
30
|
+
expect(headers).to include(expected_headers)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
context "with a query" do
|
35
|
+
let(:expected_query) {
|
36
|
+
{
|
37
|
+
"one" => "a",
|
38
|
+
"two" => "b",
|
39
|
+
}
|
40
|
+
}
|
41
|
+
|
42
|
+
let(:resp) { subject.send(:make_request, 'get', url, query: expected_query) }
|
43
|
+
|
44
|
+
it "sets the query" do
|
45
|
+
expect(resp.request.url).to include("one=a&two=b")
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,209 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'dalli'
|
3
|
+
|
4
|
+
describe Px::Service::Client::Caching do
|
5
|
+
subject {
|
6
|
+
Class.new.include(Px::Service::Client::Caching).tap do |c|
|
7
|
+
# Anonymous classes don't have a name. Stub out :name so that things work
|
8
|
+
allow(c).to receive(:name).and_return("Caching")
|
9
|
+
end.new
|
10
|
+
}
|
11
|
+
|
12
|
+
let(:dalli_host) { "localhost:11211" }
|
13
|
+
let(:dalli_options) { { :namespace => "service-client-test", expires_in: 3600, compress: false, failover: false } }
|
14
|
+
let(:dalli) { Dalli::Client.new(dalli_host, dalli_options) }
|
15
|
+
|
16
|
+
before :each do
|
17
|
+
dalli.flush_all
|
18
|
+
subject.cache_client = dalli
|
19
|
+
end
|
20
|
+
|
21
|
+
let (:url) { "http://search/foo?bar=baz"}
|
22
|
+
let (:response) {
|
23
|
+
{ "response" => ["foo", "bar"], "status" => 200 }
|
24
|
+
}
|
25
|
+
|
26
|
+
shared_examples_for "a successful request" do
|
27
|
+
it "should call the block" do
|
28
|
+
called = false
|
29
|
+
subject.cache_request(url, strategy: strategy) do |u|
|
30
|
+
expect(u).to eq(url)
|
31
|
+
called = true
|
32
|
+
end
|
33
|
+
|
34
|
+
expect(called).to be_truthy
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should return the block's return value" do
|
38
|
+
expect(subject.cache_request(url, strategy: strategy) { response }).to eq(response)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
shared_examples_for "a failed uncacheable request" do
|
43
|
+
it "should raise the exception raised by the block" do
|
44
|
+
expect{
|
45
|
+
subject.cache_request(url, strategy: strategy) do
|
46
|
+
# Px::Service::ServiceRequestError is not cachable
|
47
|
+
# and does not trigger a fallback to a cached response
|
48
|
+
raise Px::Service::ServiceRequestError.new("Error", 404)
|
49
|
+
end
|
50
|
+
}.to raise_error(Px::Service::ServiceRequestError)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
shared_examples_for "a request with no cached response" do
|
55
|
+
it "raises the exception" do
|
56
|
+
expect {
|
57
|
+
subject.cache_request(url, strategy: strategy) do
|
58
|
+
raise Px::Service::ServiceError.new("Error", 500)
|
59
|
+
end
|
60
|
+
}.to raise_error(Px::Service::ServiceError)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
context "when not caching" do
|
65
|
+
let(:strategy) { :none }
|
66
|
+
|
67
|
+
it_behaves_like "a successful request"
|
68
|
+
it_behaves_like "a failed uncacheable request"
|
69
|
+
end
|
70
|
+
|
71
|
+
context "when caching as a last resort" do
|
72
|
+
let(:strategy) { :last_resort }
|
73
|
+
|
74
|
+
it_behaves_like "a successful request"
|
75
|
+
it_behaves_like "a failed uncacheable request"
|
76
|
+
|
77
|
+
context "when there is a cached response" do
|
78
|
+
before :each do
|
79
|
+
subject.cache_request(url, strategy: strategy) do
|
80
|
+
response
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
it "returns the cached response on failure" do
|
85
|
+
expect(subject.cache_request(url, strategy: strategy) do
|
86
|
+
raise Px::Service::ServiceError.new("Error", 500)
|
87
|
+
end).to eq(response)
|
88
|
+
end
|
89
|
+
|
90
|
+
it "does not returns the cached response on request error" do
|
91
|
+
expect {
|
92
|
+
subject.cache_request(url, strategy: strategy) do
|
93
|
+
raise Px::Service::ServiceRequestError.new("Error", 404)
|
94
|
+
end
|
95
|
+
}.to raise_error(Px::Service::ServiceRequestError)
|
96
|
+
end
|
97
|
+
|
98
|
+
it "touches the cache entry on failure" do
|
99
|
+
expect(dalli).to receive(:touch).with(a_kind_of(String), a_kind_of(Fixnum))
|
100
|
+
|
101
|
+
subject.cache_request(url, strategy: strategy) do
|
102
|
+
raise Px::Service::ServiceError.new("Error", 500)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
it_behaves_like "a request with no cached response"
|
108
|
+
end
|
109
|
+
|
110
|
+
context "when caching as a first resort" do
|
111
|
+
let(:strategy) { :first_resort }
|
112
|
+
|
113
|
+
it_behaves_like "a successful request"
|
114
|
+
it_behaves_like "a failed uncacheable request"
|
115
|
+
|
116
|
+
context "when there is a cached response" do
|
117
|
+
before :each do
|
118
|
+
subject.cache_request(url, strategy: strategy) do
|
119
|
+
response
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
it "does not invoke the block" do
|
124
|
+
called = false
|
125
|
+
subject.cache_request(url, strategy: strategy) do |u|
|
126
|
+
called = true
|
127
|
+
end
|
128
|
+
|
129
|
+
expect(called).to be_falsey
|
130
|
+
end
|
131
|
+
|
132
|
+
it "returns the response" do
|
133
|
+
expect(subject.cache_request(url, strategy: strategy) { nil }).to eq(response)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
context "when there is an expired cached response" do
|
138
|
+
before :each do
|
139
|
+
Timecop.freeze(10.minutes.ago) do
|
140
|
+
subject.cache_request(url, strategy: strategy) do
|
141
|
+
response
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
let (:response) { { "value" => "response" } }
|
147
|
+
|
148
|
+
it "invokes the block" do
|
149
|
+
called = false
|
150
|
+
subject.cache_request(url, strategy: strategy) do |u|
|
151
|
+
called = true
|
152
|
+
end
|
153
|
+
|
154
|
+
expect(called).to be_truthy
|
155
|
+
end
|
156
|
+
|
157
|
+
it "returns the new response" do
|
158
|
+
expect(subject.cache_request(url, strategy: strategy) { response }).to eq(response)
|
159
|
+
end
|
160
|
+
|
161
|
+
it "updates the cache entry before making the request" do
|
162
|
+
subject.cache_request(url, strategy: strategy) do
|
163
|
+
# A bit goofy, but basically, make a request, but in the block
|
164
|
+
# check that another request that happens while we're in the block
|
165
|
+
# gets the cached result and doesn't invoke its own block
|
166
|
+
called = false
|
167
|
+
expect(subject.cache_request(url, strategy: strategy) do
|
168
|
+
called = true
|
169
|
+
end).to eq(response)
|
170
|
+
expect(called).to be_falsey
|
171
|
+
|
172
|
+
response
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
it "caches the new response" do
|
177
|
+
subject.cache_request(url, strategy: strategy) do
|
178
|
+
response
|
179
|
+
end
|
180
|
+
|
181
|
+
expect(subject.cache_request(url, strategy: strategy) { nil }).to eq(response)
|
182
|
+
end
|
183
|
+
|
184
|
+
it "returns the cached response on failure" do
|
185
|
+
expect(subject.cache_request(url, strategy: strategy) do
|
186
|
+
raise Px::Service::ServiceError.new("Error", 500)
|
187
|
+
end).to eq(response)
|
188
|
+
end
|
189
|
+
|
190
|
+
it "does not returns the cached response on request error" do
|
191
|
+
expect {
|
192
|
+
subject.cache_request(url, strategy: strategy) do
|
193
|
+
raise Px::Service::ServiceRequestError.new("Error", 404)
|
194
|
+
end
|
195
|
+
}.to raise_error(Px::Service::ServiceRequestError)
|
196
|
+
end
|
197
|
+
|
198
|
+
it "touches the cache entry on failure" do
|
199
|
+
expect(dalli).to receive(:touch).with(a_kind_of(String), a_kind_of(Fixnum)).twice
|
200
|
+
|
201
|
+
subject.cache_request(url, strategy: strategy) do
|
202
|
+
raise Px::Service::ServiceError.new("Error", 500)
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
it_behaves_like "a request with no cached response"
|
208
|
+
end
|
209
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Px::Service::Client::CircuitBreaker do
|
4
|
+
|
5
|
+
let(:subject_class) {
|
6
|
+
Class.new.include(Px::Service::Client::CircuitBreaker).tap do |c|
|
7
|
+
# Anonymous classes don't have a name. Stub out :name so that things work
|
8
|
+
allow(c).to receive(:name).and_return("CircuitBreaker")
|
9
|
+
end
|
10
|
+
}
|
11
|
+
|
12
|
+
subject { subject_class.new }
|
13
|
+
|
14
|
+
describe '#included' do
|
15
|
+
it "excludes Px::Service::ServiceRequestError by default" do
|
16
|
+
expect(subject_class.circuit_handler.excluded_exceptions).to include(Px::Service::ServiceRequestError)
|
17
|
+
end
|
18
|
+
|
19
|
+
it "sets the failure threshold" do
|
20
|
+
expect(subject_class.circuit_handler.failure_threshold).to eq(5)
|
21
|
+
end
|
22
|
+
|
23
|
+
it "sets the failure timeout" do
|
24
|
+
expect(subject_class.circuit_handler.failure_timeout).to eq(7)
|
25
|
+
end
|
26
|
+
|
27
|
+
it "sets the invocation timeout" do
|
28
|
+
expect(subject_class.circuit_handler.invocation_timeout).to eq(5)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
describe '#circuit_method' do
|
33
|
+
context "when the wrapped method succeeds" do
|
34
|
+
before :each do
|
35
|
+
subject_class.send(:define_method, :test_method) do |arg|
|
36
|
+
"returned #{arg}"
|
37
|
+
end
|
38
|
+
|
39
|
+
subject_class.circuit_method(:test_method)
|
40
|
+
end
|
41
|
+
|
42
|
+
it "returns the return value" do
|
43
|
+
expect(subject.test_method("test")).to eq("returned test")
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
context "when the wrapped method fails with a ServiceRequestError" do
|
48
|
+
before :each do
|
49
|
+
subject_class.send(:define_method, :test_method) do |arg|
|
50
|
+
raise Px::Service::ServiceRequestError.new("Error", 404)
|
51
|
+
end
|
52
|
+
|
53
|
+
subject_class.circuit_method(:test_method)
|
54
|
+
end
|
55
|
+
|
56
|
+
it "raises a ServiceRequestError" do
|
57
|
+
expect{
|
58
|
+
subject.test_method("test")
|
59
|
+
}.to raise_error(Px::Service::ServiceRequestError, "Error")
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
context "when the wrapped method fails with a ServiceError" do
|
64
|
+
before :each do
|
65
|
+
subject_class.send(:define_method, :test_method) do |arg|
|
66
|
+
raise Px::Service::ServiceError.new("Error", 500)
|
67
|
+
end
|
68
|
+
|
69
|
+
subject_class.circuit_method(:test_method)
|
70
|
+
end
|
71
|
+
|
72
|
+
it "raises a ServiceError" do
|
73
|
+
expect{
|
74
|
+
subject.test_method("test")
|
75
|
+
}.to raise_error(Px::Service::ServiceError, "Error")
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
context "when the wrapped method fails with another exception" do
|
80
|
+
before :each do
|
81
|
+
subject_class.send(:define_method, :test_method) do |arg|
|
82
|
+
this_is_not_a_method # Raises NoMethodError
|
83
|
+
end
|
84
|
+
|
85
|
+
subject_class.circuit_method(:test_method)
|
86
|
+
end
|
87
|
+
|
88
|
+
it "raises a ServiceError" do
|
89
|
+
expect{
|
90
|
+
subject.test_method("test")
|
91
|
+
}.to raise_error(Px::Service::ServiceError)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
context "when the circuit is open" do
|
96
|
+
before :each do
|
97
|
+
subject_class.send(:define_method, :test_method) do |arg|
|
98
|
+
"should not be called"
|
99
|
+
end
|
100
|
+
|
101
|
+
subject_class.circuit_method(:test_method)
|
102
|
+
|
103
|
+
subject.circuit_state.trip
|
104
|
+
end
|
105
|
+
|
106
|
+
it "raises a ServiceError" do
|
107
|
+
expect{
|
108
|
+
subject.test_method("test")
|
109
|
+
}.to raise_error(Px::Service::ServiceError)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|