px-service-client 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|