shift-circuit-breaker 0.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 +7 -0
- data/.codeclimate.yml +11 -0
- data/.gitignore +13 -0
- data/.reek +21 -0
- data/.rspec +3 -0
- data/.rubocop.yml +21 -0
- data/CONTRIBUTING.md +80 -0
- data/Gemfile +2 -0
- data/LICENSE +21 -0
- data/README.md +81 -0
- data/Rakefile +9 -0
- data/bin/console +7 -0
- data/bin/setup +7 -0
- data/config/newrelic.yml +28 -0
- data/config/rubocop/.layout_rubocop.yml +2 -0
- data/config/rubocop/.lint_rubocop.yml +47 -0
- data/config/rubocop/.metrics_rubocop.yml +39 -0
- data/config/rubocop/.naming_rubocop.yml +11 -0
- data/config/rubocop/.performance_rubocop.yml +60 -0
- data/config/rubocop/.style_rubocop.yml +154 -0
- data/lib/shift/circuit_breaker.rb +57 -0
- data/lib/shift/circuit_breaker/adapters/base_adapter.rb +13 -0
- data/lib/shift/circuit_breaker/adapters/newrelic_adapter.rb +13 -0
- data/lib/shift/circuit_breaker/adapters/sentry_adapter.rb +13 -0
- data/lib/shift/circuit_breaker/circuit_handler.rb +107 -0
- data/lib/shift/circuit_breaker/circuit_logger.rb +36 -0
- data/lib/shift/circuit_breaker/circuit_monitor.rb +32 -0
- data/lib/shift/circuit_breaker/config.rb +48 -0
- data/lib/shift/circuit_breaker/version.rb +7 -0
- data/shift-circuit-breaker.gemspec +40 -0
- data/spec/shift/adapters/base_adapter_spec.rb +21 -0
- data/spec/shift/adapters/newrelic_adapter_spec.rb +44 -0
- data/spec/shift/adapters/sentry_adapter_spec.rb +42 -0
- data/spec/shift/circuit_breaker/circuit_handler_exception_handling_spec.rb +142 -0
- data/spec/shift/circuit_breaker/circuit_handler_monitoring_spec.rb +72 -0
- data/spec/shift/circuit_breaker/circuit_handler_spec.rb +165 -0
- data/spec/shift/circuit_breaker/circuit_logger_spec.rb +67 -0
- data/spec/shift/circuit_breaker/circuit_monitor_spec.rb +52 -0
- data/spec/shift/circuit_breaker_spec.rb +73 -0
- data/spec/spec_helper.rb +25 -0
- metadata +230 -0
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# -*- encoding: utf-8 -*-
|
3
|
+
|
4
|
+
$:.push File.expand_path("../lib", __FILE__)
|
5
|
+
|
6
|
+
require "shift/circuit_breaker/version"
|
7
|
+
|
8
|
+
Gem::Specification.new do |s|
|
9
|
+
s.name = "shift-circuit-breaker"
|
10
|
+
s.version = Shift::CircuitBreaker::VERSION
|
11
|
+
s.platform = Gem::Platform::RUBY
|
12
|
+
s.summary = "A generic implementation of the Circuit Breaker pattern in Ruby"
|
13
|
+
s.description = <<-DOC
|
14
|
+
The library provides a mechanism for detecting, monitoring and controlling external service calls that will
|
15
|
+
most-likely fail at some point (e.g. timeout) and cause request queuing, thus preventing cascading system failures.
|
16
|
+
DOC
|
17
|
+
s.homepage = "https://github.com/shiftcommerce/shift-circuit-breaker"
|
18
|
+
s.authors = ["Mufudzi Masaire"]
|
19
|
+
s.email = "mufudzi.masaire@shiftcommerce.com"
|
20
|
+
s.date = Time.now.strftime("%Y-%m-%d")
|
21
|
+
s.license = "MIT"
|
22
|
+
|
23
|
+
s.required_ruby_version = ">= 2.3.0"
|
24
|
+
s.require_paths = ["lib"]
|
25
|
+
|
26
|
+
s.files = `git ls-files`.split("\n")
|
27
|
+
s.test_files = `git ls-files -- { test, spec, features }/*`.split("\n")
|
28
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
|
29
|
+
|
30
|
+
s.add_runtime_dependency "activesupport", "~> 5.1", ">= 5.1.4"
|
31
|
+
s.add_runtime_dependency "newrelic_rpm", "~> 4.8", ">= 4.8.0.341"
|
32
|
+
s.add_runtime_dependency "sentry-raven", "~> 2.7", ">= 2.7.2"
|
33
|
+
|
34
|
+
s.add_development_dependency "bundler", "~> 1.16"
|
35
|
+
s.add_development_dependency "pry", "~> 0.11.3"
|
36
|
+
s.add_development_dependency "rake", "~> 12.3"
|
37
|
+
s.add_development_dependency "rspec", "~> 3.7"
|
38
|
+
s.add_development_dependency "rubocop", "~> 0.52.1"
|
39
|
+
s.add_development_dependency "timecop", "~> 0.9.1"
|
40
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
|
5
|
+
module Shift
|
6
|
+
module CircuitBreaker
|
7
|
+
module Adapters
|
8
|
+
describe BaseAdapter do
|
9
|
+
context "#call" do
|
10
|
+
it "raises NotImplementedError" do
|
11
|
+
# Arrange
|
12
|
+
message = "some message"
|
13
|
+
|
14
|
+
# Act & Assert
|
15
|
+
expect { described_class.call(message) }.to raise_error(NotImplementedError)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
|
5
|
+
module NewRelic
|
6
|
+
module Agent
|
7
|
+
def self.increment_metric(*); end
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
module Shift
|
12
|
+
module CircuitBreaker
|
13
|
+
module Adapters
|
14
|
+
describe NewRelicAdapter do
|
15
|
+
it "is a subclass of 'Shift::CircuitBreaker::Adapters::BaseAdapter'" do
|
16
|
+
expect(described_class).to be < Shift::CircuitBreaker::Adapters::BaseAdapter
|
17
|
+
end
|
18
|
+
|
19
|
+
context "#call" do
|
20
|
+
it "does not raise NotImplementedError" do
|
21
|
+
# Arrange
|
22
|
+
metric = "Custom/TestCircuitBreaker/closed"
|
23
|
+
|
24
|
+
# Act & Assert
|
25
|
+
expect { described_class.call(metric) }.not_to raise_error(NotImplementedError)
|
26
|
+
end
|
27
|
+
|
28
|
+
it "calls NewRelic::Agent#increment_metric" do
|
29
|
+
# Arrange
|
30
|
+
metric = "Custom/TestCircuitBreaker/closed"
|
31
|
+
|
32
|
+
allow(::NewRelic::Agent).to receive(:increment_metric)
|
33
|
+
|
34
|
+
# Act
|
35
|
+
described_class.call(metric)
|
36
|
+
|
37
|
+
# Assert
|
38
|
+
expect(::NewRelic::Agent).to have_received(:increment_metric).with(metric).once
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
|
5
|
+
module Sentry
|
6
|
+
def self.capture_exception(*); end
|
7
|
+
end
|
8
|
+
|
9
|
+
module Shift
|
10
|
+
module CircuitBreaker
|
11
|
+
module Adapters
|
12
|
+
describe SentryAdapter do
|
13
|
+
it "is a subclass of 'Shift::CircuitBreaker::Adapters::BaseAdapter'" do
|
14
|
+
expect(described_class).to be < Shift::CircuitBreaker::Adapters::BaseAdapter
|
15
|
+
end
|
16
|
+
|
17
|
+
context "#call" do
|
18
|
+
it "does not raise NotImplementedError" do
|
19
|
+
# Arrange
|
20
|
+
message = "some message"
|
21
|
+
|
22
|
+
# Act & Assert
|
23
|
+
expect { described_class.call(message) }.not_to raise_error(NotImplementedError)
|
24
|
+
end
|
25
|
+
|
26
|
+
it "calls Sentry#capture_exception" do
|
27
|
+
# Arrange
|
28
|
+
message = "some exception"
|
29
|
+
|
30
|
+
allow(::Raven).to receive(:capture_exception)
|
31
|
+
|
32
|
+
# Act
|
33
|
+
described_class.call(message)
|
34
|
+
|
35
|
+
# Assert
|
36
|
+
expect(::Raven).to have_received(:capture_exception).with(message).once
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,142 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
|
5
|
+
module Shift
|
6
|
+
module CircuitBreaker
|
7
|
+
describe CircuitHandler do
|
8
|
+
before do
|
9
|
+
allow($stdout).to receive(:write)
|
10
|
+
end
|
11
|
+
|
12
|
+
context "Exception Handling" do
|
13
|
+
let(:default_error_threshold) { 10 }
|
14
|
+
let(:default_skip_duration) { 60 }
|
15
|
+
|
16
|
+
context "when a timeout exception is raised" do
|
17
|
+
it "returns the fallback" do
|
18
|
+
# Arrange
|
19
|
+
operation_stub = instance_double("Operation")
|
20
|
+
fallback_stub = instance_double("Fallback")
|
21
|
+
|
22
|
+
allow(operation_stub).to receive(:perform_task).and_raise(Timeout::Error, "Request Timeout")
|
23
|
+
|
24
|
+
# Act
|
25
|
+
cb = described_class.new(:test_circuit_breaker, error_threshold: default_error_threshold, skip_duration: default_skip_duration)
|
26
|
+
operation_result = cb.call(operation: -> { operation_stub.perform_task }, fallback: -> { fallback_stub })
|
27
|
+
|
28
|
+
# Assert
|
29
|
+
expect(operation_result).to eq(fallback_stub)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
context "when the error_threshold is exceeded" do
|
34
|
+
it "opens the curcuit returns the fallback" do
|
35
|
+
# Arrange
|
36
|
+
operation_1_stub = instance_double("Operation1")
|
37
|
+
operation_2_stub = instance_double("Operation2")
|
38
|
+
operation_3_stub = instance_double("Operation3")
|
39
|
+
fallback_stub = instance_double("Fallback")
|
40
|
+
|
41
|
+
allow(operation_1_stub).to receive(:perform_task).and_raise(Timeout::Error, "Request Timeout")
|
42
|
+
allow(operation_2_stub).to receive(:perform_task).and_raise(Timeout::Error, "Request Timeout")
|
43
|
+
allow(operation_3_stub).to receive(:perform_task).and_raise(Timeout::Error, "Request Timeout")
|
44
|
+
|
45
|
+
# Act & Assert
|
46
|
+
cb = described_class.new(:test_circuit_breaker, error_threshold: 2, skip_duration: default_skip_duration)
|
47
|
+
|
48
|
+
# The first operation will fail with Timeout::Error, resulting in the exception being caught and
|
49
|
+
# the fallback being returned as the operation result. The error_count is incremented to 1.
|
50
|
+
aggregate_failures do
|
51
|
+
expect(operation_1_stub).to receive(:perform_task)
|
52
|
+
operation_1_result = cb.call(operation: -> { operation_1_stub.perform_task }, fallback: -> { fallback_stub })
|
53
|
+
# Check Circuit Breaker state and result
|
54
|
+
expect(cb.state).to eq(:closed)
|
55
|
+
expect(cb.error_count).to eq(1)
|
56
|
+
expect(operation_1_result).to eq(fallback_stub)
|
57
|
+
end
|
58
|
+
|
59
|
+
# The second operation will also fail with Timeout::Error, resulting in the exception being caught and
|
60
|
+
# the fallback being returned as the operation result. The error_count is incremented to 2 and the circuit
|
61
|
+
# is opened.
|
62
|
+
aggregate_failures do
|
63
|
+
expect(operation_2_stub).to receive(:perform_task)
|
64
|
+
operation_2_result = cb.call(operation: -> { operation_2_stub.perform_task }, fallback: -> { fallback_stub })
|
65
|
+
# Check Circuit Breaker state and result
|
66
|
+
expect(cb.state).to eq(:open)
|
67
|
+
expect(cb.error_count).to eq(2)
|
68
|
+
expect(operation_2_result).to eq(fallback_stub)
|
69
|
+
end
|
70
|
+
|
71
|
+
# The third operation will not be run as the circuit is open. This should result in #call not being executed
|
72
|
+
# and the fallback being returned early.
|
73
|
+
aggregate_failures do
|
74
|
+
expect(operation_3_stub).not_to receive(:perform_task)
|
75
|
+
operation_3_result = cb.call(operation: -> { operation_3_stub.perform_task }, fallback: -> { fallback_stub })
|
76
|
+
# Check Circuit Breaker state and result
|
77
|
+
expect(cb.state).to eq(:open)
|
78
|
+
expect(cb.error_count).to eq(2)
|
79
|
+
expect(operation_3_result).to eq(fallback_stub)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
context "when the error_threshold is exceeded and skip_duration has expired" do
|
85
|
+
after { Timecop.return }
|
86
|
+
|
87
|
+
it "closes the circuit and returns the operation result" do
|
88
|
+
# Arrange
|
89
|
+
operation_1_stub = instance_double("Operation1")
|
90
|
+
operation_2_stub = instance_double("Operation2")
|
91
|
+
operation_3_stub = instance_double("Operation3")
|
92
|
+
fallback_stub = instance_double("Fallback")
|
93
|
+
expected_result_stub = instance_double("ExpectedResult")
|
94
|
+
|
95
|
+
allow(operation_1_stub).to receive(:perform_task).and_raise(Timeout::Error, "Request Timeout")
|
96
|
+
allow(operation_2_stub).to receive(:perform_task).and_raise(Timeout::Error, "Request Timeout")
|
97
|
+
allow(operation_3_stub).to receive(:perform_task).and_return(expected_result_stub)
|
98
|
+
|
99
|
+
# Act & Assert
|
100
|
+
cb = described_class.new(:test_circuit_breaker, error_threshold: 2, skip_duration: 10)
|
101
|
+
|
102
|
+
# The first operation will fail with Timeout::Error, resulting in the exception being caught and
|
103
|
+
# the fallback being returned as the operation result. The error_count is incremented to 1.
|
104
|
+
aggregate_failures do
|
105
|
+
expect(operation_1_stub).to receive(:perform_task)
|
106
|
+
operation_1_result = cb.call(operation: -> { operation_1_stub.perform_task }, fallback: -> { fallback_stub })
|
107
|
+
# Check Circuit Breaker state and result
|
108
|
+
expect(cb.state).to eq(:closed)
|
109
|
+
expect(cb.error_count).to eq(1)
|
110
|
+
expect(operation_1_result).to eq(fallback_stub)
|
111
|
+
end
|
112
|
+
|
113
|
+
# The second operation will also fail with Timeout::Error, resulting in the exception being caught and
|
114
|
+
# the fallback being returned as the operation result. The error_count is incremented to 2 and the circuit
|
115
|
+
# is opened.
|
116
|
+
aggregate_failures do
|
117
|
+
expect(operation_2_stub).to receive(:perform_task)
|
118
|
+
operation_2_result = cb.call(operation: -> { operation_2_stub.perform_task }, fallback: -> { fallback_stub })
|
119
|
+
# Check Circuit Breaker state and result
|
120
|
+
expect(cb.state).to eq(:open)
|
121
|
+
expect(cb.error_count).to eq(2)
|
122
|
+
expect(operation_2_result).to eq(fallback_stub)
|
123
|
+
end
|
124
|
+
|
125
|
+
# The third request is fired after the skip duration has expired. It should the allowed to execute,
|
126
|
+
# ie. #call should be executed, and the expected operation result returned.
|
127
|
+
Timecop.travel(Time.now + 10.seconds) do
|
128
|
+
aggregate_failures do
|
129
|
+
expect(operation_3_stub).to receive(:perform_task)
|
130
|
+
operation_3_result = cb.call(operation: -> { operation_3_stub.perform_task }, fallback: -> { fallback_stub })
|
131
|
+
|
132
|
+
# Check Circuit Breaker state and result
|
133
|
+
expect(cb.state).to eq(:closed)
|
134
|
+
expect(operation_3_result).to eq(expected_result_stub)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
|
5
|
+
module Shift
|
6
|
+
module CircuitBreaker
|
7
|
+
describe CircuitHandler do
|
8
|
+
before do
|
9
|
+
allow($stdout).to receive(:write)
|
10
|
+
end
|
11
|
+
|
12
|
+
context "Monitoring" do
|
13
|
+
let(:default_error_threshold) { 10 }
|
14
|
+
let(:default_skip_duration) { 60 }
|
15
|
+
|
16
|
+
context "when an operation is successful" do
|
17
|
+
it "records the metric" do
|
18
|
+
# Arrange
|
19
|
+
monitor_stub = instance_double("Shift::CircuitMonitor")
|
20
|
+
operation_stub = instance_double("Operation")
|
21
|
+
fallback_stub = instance_double("Fallback")
|
22
|
+
expected_result_stub = instance_double("ExpectedResult")
|
23
|
+
|
24
|
+
allow(monitor_stub).to receive(:record_metric)
|
25
|
+
allow(operation_stub).to receive(:perform_task).and_return(expected_result_stub)
|
26
|
+
|
27
|
+
# Act
|
28
|
+
cb = described_class.new(:test_circuit_breaker,
|
29
|
+
error_threshold: default_skip_duration,
|
30
|
+
skip_duration: default_skip_duration,
|
31
|
+
monitor: monitor_stub)
|
32
|
+
|
33
|
+
# Assert
|
34
|
+
aggregate_failures do
|
35
|
+
expect(operation_stub).to receive(:perform_task).and_return(expected_result_stub)
|
36
|
+
|
37
|
+
operation_result = cb.call(operation: -> { operation_stub.perform_task }, fallback: -> { fallback_stub })
|
38
|
+
|
39
|
+
expect(monitor_stub).to have_received(:record_metric)
|
40
|
+
expect(operation_result).to eq(expected_result_stub)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
context "when an exception is raised" do
|
46
|
+
it "records the metric" do
|
47
|
+
# Arrange
|
48
|
+
monitor_stub = instance_double("Shift::CircuitMonitor")
|
49
|
+
operation_stub = instance_double("Operation")
|
50
|
+
fallback_stub = instance_double("Fallback")
|
51
|
+
|
52
|
+
allow(monitor_stub).to receive(:record_metric)
|
53
|
+
allow(operation_stub).to receive(:perform_task).and_raise(Timeout::Error, "Request Timeout")
|
54
|
+
|
55
|
+
# Act
|
56
|
+
cb = described_class.new(:test_circuit_breaker, error_threshold: 1, skip_duration: default_skip_duration, monitor: monitor_stub)
|
57
|
+
|
58
|
+
# Assert
|
59
|
+
aggregate_failures do
|
60
|
+
expect(operation_stub).to receive(:perform_task).and_raise(Timeout::Error, "Request Timeout")
|
61
|
+
|
62
|
+
operation_result = cb.call(operation: -> { operation_stub.perform_task }, fallback: -> { fallback_stub })
|
63
|
+
|
64
|
+
expect(monitor_stub).to have_received(:record_metric)
|
65
|
+
expect(operation_result).to eq(fallback_stub)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,165 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
|
5
|
+
module Shift
|
6
|
+
module CircuitBreaker
|
7
|
+
describe CircuitHandler do
|
8
|
+
before do
|
9
|
+
allow($stdout).to receive(:write)
|
10
|
+
end
|
11
|
+
|
12
|
+
context "when given a valid operation" do
|
13
|
+
let(:default_error_threshold) { 10 }
|
14
|
+
let(:default_skip_duration) { 60 }
|
15
|
+
|
16
|
+
it "returns the expected result" do
|
17
|
+
# Arrange
|
18
|
+
operation_stub = instance_double("Operation")
|
19
|
+
fallback_stub = instance_double("Fallback")
|
20
|
+
expected_result_stub = instance_double("ExpectedResult")
|
21
|
+
|
22
|
+
allow(operation_stub).to receive(:perform_task).and_return(expected_result_stub)
|
23
|
+
|
24
|
+
# Act
|
25
|
+
cb = described_class.new(:test_circuit_breaker, error_threshold: default_error_threshold, skip_duration: default_skip_duration)
|
26
|
+
operation_result = cb.call(operation: -> { operation_stub.perform_task }, fallback: -> { fallback_stub })
|
27
|
+
|
28
|
+
# Assert
|
29
|
+
expect(operation_result).to eq(expected_result_stub)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
context "when given additional exception classes" do
|
34
|
+
let(:default_error_threshold) { 10 }
|
35
|
+
let(:default_skip_duration) { 60 }
|
36
|
+
|
37
|
+
it "rescues the exception and returns the fallback" do
|
38
|
+
# Arrange
|
39
|
+
operation_stub = instance_double("Operation")
|
40
|
+
fallback_stub = instance_double("Fallback")
|
41
|
+
additional_exception_classes = [Faraday::ClientError]
|
42
|
+
|
43
|
+
allow(operation_stub).to receive(:perform_task).and_raise(Faraday::ClientError, "client error")
|
44
|
+
|
45
|
+
# Act
|
46
|
+
cb = described_class.new(:test_circuit_breaker,
|
47
|
+
error_threshold: default_error_threshold,
|
48
|
+
skip_duration: default_skip_duration,
|
49
|
+
additional_exception_classes: additional_exception_classes)
|
50
|
+
|
51
|
+
operation_result = cb.call(operation: -> { operation_stub.perform_task }, fallback: -> { fallback_stub })
|
52
|
+
|
53
|
+
# Assert
|
54
|
+
expect(operation_result).to eq(fallback_stub)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
context "Invalid Arguments" do
|
59
|
+
context "when initialising service" do
|
60
|
+
let(:default_error_threshold) { 10 }
|
61
|
+
let(:default_skip_duration) { 60 }
|
62
|
+
|
63
|
+
context "when no error_threshold is provided" do
|
64
|
+
it "raises an ArgumentError" do
|
65
|
+
# Act & Assert
|
66
|
+
expect { described_class.new(:test_circuit_breaker, skip_duration: default_skip_duration) }.to raise_error(ArgumentError)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
context "when no skip_duration is provided" do
|
71
|
+
it "raises an ArgumentError" do
|
72
|
+
# Act & Assert
|
73
|
+
expect { described_class.new(:test_circuit_breaker, error_threshold: default_skip_duration) }.to raise_error(ArgumentError)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
context "Invalid #call arguments" do
|
79
|
+
let(:default_error_threshold) { 10 }
|
80
|
+
let(:default_skip_duration) { 60 }
|
81
|
+
|
82
|
+
context "when no operation is given" do
|
83
|
+
it "raises an ArgumentError" do
|
84
|
+
# Arrange
|
85
|
+
fallback_stub = instance_double("Fallback")
|
86
|
+
|
87
|
+
# Act
|
88
|
+
cb = described_class.new(:test_circuit_breaker, error_threshold: default_error_threshold, skip_duration: default_skip_duration)
|
89
|
+
|
90
|
+
# Assert
|
91
|
+
expect { cb.call(fallback: -> { fallback_stub }) }.to raise_error(ArgumentError)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
context "when no fallback is given" do
|
96
|
+
it "raises an ArgumentError" do
|
97
|
+
# Arrange
|
98
|
+
operation_stub = instance_double("Operation")
|
99
|
+
|
100
|
+
# Act
|
101
|
+
cb = described_class.new(:test_circuit_breaker, error_threshold: default_error_threshold, skip_duration: default_skip_duration)
|
102
|
+
|
103
|
+
# Act & Assert
|
104
|
+
expect { cb.call(operation: -> { operation_stub }) }.to raise_error(ArgumentError)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
context "when given nil as the operation" do
|
109
|
+
it "raises an ArgumentError" do
|
110
|
+
# Arrange
|
111
|
+
fallback_stub = instance_double("Fallback")
|
112
|
+
|
113
|
+
# Act
|
114
|
+
cb = described_class.new(:test_circuit_breaker, error_threshold: default_error_threshold, skip_duration: default_skip_duration)
|
115
|
+
|
116
|
+
# Assert
|
117
|
+
expect { cb.call(operation: nil, fallback: -> { fallback_stub }) }.to raise_error(ArgumentError)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
context "when given nil as the fallback" do
|
122
|
+
it "raises an ArgumentError" do
|
123
|
+
# Arrange
|
124
|
+
operation_stub = instance_double("Operation")
|
125
|
+
|
126
|
+
# Act
|
127
|
+
cb = described_class.new(:test_circuit_breaker, error_threshold: default_error_threshold, skip_duration: default_skip_duration)
|
128
|
+
|
129
|
+
# Assert
|
130
|
+
expect { cb.call(operation: -> { operation_stub }, fallback: nil) }.to raise_error(ArgumentError)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
context "when given an operation that does not implement #call" do
|
135
|
+
it "raises an ArgumentError" do
|
136
|
+
# Arrange
|
137
|
+
operation_stub = instance_double("Operation")
|
138
|
+
fallback_stub = instance_double("Fallback")
|
139
|
+
|
140
|
+
# Act
|
141
|
+
cb = described_class.new(:test_circuit_breaker, error_threshold: default_error_threshold, skip_duration: default_skip_duration)
|
142
|
+
|
143
|
+
# Assert
|
144
|
+
expect { cb.call(operation: operation_stub, fallback: -> { fallback_stub }) }.to raise_error(ArgumentError)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
context "when given a fallback that does not implement #call" do
|
149
|
+
it "raises an ArgumentError" do
|
150
|
+
# Arrange
|
151
|
+
operation_stub = instance_double("Operation")
|
152
|
+
fallback_stub = instance_double("Fallback")
|
153
|
+
|
154
|
+
# Act
|
155
|
+
cb = described_class.new(:test_circuit_breaker, error_threshold: default_error_threshold, skip_duration: default_skip_duration)
|
156
|
+
|
157
|
+
# Assert
|
158
|
+
expect { cb.call(operation: -> { operation_stub }, fallback: fallback_stub) }.to raise_error(ArgumentError)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|