circuitbox 1.1.0 → 2.0.0.pre4
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 +5 -5
- data/README.md +56 -187
- data/lib/circuitbox.rb +14 -57
- data/lib/circuitbox/circuit_breaker.rb +137 -161
- data/lib/circuitbox/circuit_breaker/logger_messages.rb +31 -0
- data/lib/circuitbox/configuration.rb +51 -0
- data/lib/circuitbox/errors/error.rb +3 -2
- data/lib/circuitbox/errors/open_circuit_error.rb +3 -1
- data/lib/circuitbox/errors/service_failure_error.rb +5 -1
- data/lib/circuitbox/excon_middleware.rb +23 -30
- data/lib/circuitbox/faraday_middleware.rb +43 -63
- data/lib/circuitbox/memory_store.rb +85 -0
- data/lib/circuitbox/memory_store/container.rb +30 -0
- data/lib/circuitbox/memory_store/monotonic_time.rb +13 -0
- data/lib/circuitbox/notifier/active_support.rb +19 -0
- data/lib/circuitbox/notifier/null.rb +13 -0
- data/lib/circuitbox/timer.rb +51 -0
- data/lib/circuitbox/version.rb +3 -1
- metadata +106 -118
- data/.gitignore +0 -20
- data/.ruby-version +0 -1
- data/.travis.yml +0 -9
- data/Gemfile +0 -6
- data/Rakefile +0 -18
- data/benchmark/circuit_store_benchmark.rb +0 -114
- data/circuitbox.gemspec +0 -48
- data/lib/circuitbox/memcache_store.rb +0 -31
- data/lib/circuitbox/notifier.rb +0 -34
- data/test/circuit_breaker_test.rb +0 -428
- data/test/circuitbox_test.rb +0 -45
- data/test/excon_middleware_test.rb +0 -131
- data/test/faraday_middleware_test.rb +0 -175
- data/test/integration/circuitbox_cross_process_open_test.rb +0 -56
- data/test/integration/faraday_middleware_test.rb +0 -78
- data/test/integration_helper.rb +0 -48
- data/test/notifier_test.rb +0 -21
- data/test/service_failure_error_test.rb +0 -23
- data/test/test_helper.rb +0 -15
data/.gitignore
DELETED
data/.ruby-version
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
2.2.2
|
data/.travis.yml
DELETED
data/Gemfile
DELETED
data/Rakefile
DELETED
@@ -1,18 +0,0 @@
|
|
1
|
-
require 'rake/testtask'
|
2
|
-
require "bundler/gem_version_tasks"
|
3
|
-
|
4
|
-
Rake::TestTask.new do |t|
|
5
|
-
t.libs << 'test'
|
6
|
-
t.test_files = FileList['test/**/*_test.rb']
|
7
|
-
end
|
8
|
-
|
9
|
-
desc "run the circuitbox benchmark scripts"
|
10
|
-
task :benchmark do
|
11
|
-
benchmark_scripts = FileList.new("./benchmark/*_benchmark.rb")
|
12
|
-
benchmark_scripts.each do |script|
|
13
|
-
system "bundle exec ruby #{script}"
|
14
|
-
end
|
15
|
-
end
|
16
|
-
|
17
|
-
desc "Run tests"
|
18
|
-
task :default => :test
|
@@ -1,114 +0,0 @@
|
|
1
|
-
require 'circuitbox'
|
2
|
-
require 'benchmark'
|
3
|
-
require 'pstore'
|
4
|
-
require 'tempfile'
|
5
|
-
require 'tmpdir'
|
6
|
-
require 'lmdb'
|
7
|
-
require 'pry'
|
8
|
-
|
9
|
-
|
10
|
-
class Circuitbox
|
11
|
-
class CircuitBreaker
|
12
|
-
# silence the circuitbreaker logger
|
13
|
-
DEV_NULL = (RUBY_PLATFORM =~ /mswin|mingw/ ? "NUL" : "/dev/null")
|
14
|
-
def logger
|
15
|
-
@_dev_null_logger ||= Logger.new DEV_NULL
|
16
|
-
end
|
17
|
-
end
|
18
|
-
end
|
19
|
-
|
20
|
-
def service
|
21
|
-
# 10% success rate to make the circuitbreaker flip flop
|
22
|
-
if rand(10) <= 0
|
23
|
-
"success"
|
24
|
-
else
|
25
|
-
raise RuntimeError, "fail"
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
|
-
def run_flip_flopping circuit
|
30
|
-
circuit.run { service }
|
31
|
-
circuit.try_close_next_time if circuit.open?
|
32
|
-
end
|
33
|
-
|
34
|
-
def without_gc
|
35
|
-
GC.start
|
36
|
-
GC.disable
|
37
|
-
yield
|
38
|
-
GC.enable
|
39
|
-
end
|
40
|
-
|
41
|
-
def benchmark_circuitbox_method_with_reporter method, reporter
|
42
|
-
without_gc { send(method, reporter) }
|
43
|
-
Circuitbox.reset
|
44
|
-
end
|
45
|
-
|
46
|
-
def circuit_with_cache cache
|
47
|
-
Circuitbox.circuit :performance, CIRCUIT_OPTIONS.merge(cache: cache)
|
48
|
-
end
|
49
|
-
|
50
|
-
CIRCUIT_OPTIONS = {
|
51
|
-
exceptions: [RuntimeError],
|
52
|
-
sleep_window: 0,
|
53
|
-
time_window: 1
|
54
|
-
}
|
55
|
-
|
56
|
-
RUNS = 10000
|
57
|
-
|
58
|
-
def circuit_store_memory_one_process reporter
|
59
|
-
circuit = circuit_with_cache Moneta.new(:Memory)
|
60
|
-
|
61
|
-
reporter.report "memory:" do
|
62
|
-
RUNS.times { run_flip_flopping circuit }
|
63
|
-
end
|
64
|
-
|
65
|
-
circuit.circuit_store.close
|
66
|
-
end
|
67
|
-
|
68
|
-
def circuit_store_pstore_one_process reporter
|
69
|
-
Tempfile.create("test_circuit_store_pstore_one_process") do |dbfile|
|
70
|
-
circuit = circuit_with_cache Moneta.new(:PStore, file: dbfile)
|
71
|
-
|
72
|
-
reporter.report "pstore:" do
|
73
|
-
RUNS.times { run_flip_flopping circuit }
|
74
|
-
end
|
75
|
-
|
76
|
-
circuit.circuit_store.close
|
77
|
-
end
|
78
|
-
end
|
79
|
-
|
80
|
-
def circuit_store_lmdb_one_process reporter
|
81
|
-
Dir.mktmpdir("test_circuit_store_lmdb_one_process") do |dbdir|
|
82
|
-
circuit = circuit_with_cache Moneta.new(:LMDB, dir: dbdir, db: "circuitbox_lmdb")
|
83
|
-
|
84
|
-
reporter.report "lmdb:" do
|
85
|
-
RUNS.times { run_flip_flopping circuit }
|
86
|
-
end
|
87
|
-
|
88
|
-
circuit.circuit_store.close
|
89
|
-
end
|
90
|
-
end
|
91
|
-
|
92
|
-
def circuit_store_daybreak_one_process reporter
|
93
|
-
Tempfile.create("test_circuit_store_daybreak_one_process") do |dbfile|
|
94
|
-
circuit = circuit_with_cache Moneta.new(:Daybreak, file: dbfile)
|
95
|
-
|
96
|
-
reporter.report "daybreak:" do
|
97
|
-
RUNS.times { run_flip_flopping circuit }
|
98
|
-
end
|
99
|
-
|
100
|
-
circuit.circuit_store.close
|
101
|
-
end
|
102
|
-
end
|
103
|
-
|
104
|
-
Benchmark.bm(8) do |x|
|
105
|
-
benchmark_circuitbox_method_with_reporter :circuit_store_memory_one_process, x
|
106
|
-
benchmark_circuitbox_method_with_reporter :circuit_store_lmdb_one_process, x
|
107
|
-
benchmark_circuitbox_method_with_reporter :circuit_store_pstore_one_process, x
|
108
|
-
benchmark_circuitbox_method_with_reporter :circuit_store_daybreak_one_process, x
|
109
|
-
end
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
data/circuitbox.gemspec
DELETED
@@ -1,48 +0,0 @@
|
|
1
|
-
# coding: utf-8
|
2
|
-
lib = File.expand_path('../lib', __FILE__)
|
3
|
-
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
-
require 'circuitbox/version'
|
5
|
-
|
6
|
-
ruby_2_2_2_plus = Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.2.2")
|
7
|
-
|
8
|
-
Gem::Specification.new do |spec|
|
9
|
-
spec.name = "circuitbox"
|
10
|
-
spec.version = Circuitbox::VERSION
|
11
|
-
spec.authors = ["Fahim Ferdous"]
|
12
|
-
spec.email = ["fahimfmf@gmail.com"]
|
13
|
-
spec.description = %q{A robust circuit breaker that manages failing external services.}
|
14
|
-
spec.summary = %q{A robust circuit breaker that manages failing external services.}
|
15
|
-
spec.homepage = ""
|
16
|
-
spec.license = "MIT"
|
17
|
-
|
18
|
-
spec.files = `git ls-files`.split($/)
|
19
|
-
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
20
|
-
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
21
|
-
spec.require_paths = ["lib"]
|
22
|
-
|
23
|
-
spec.add_development_dependency "bundler"
|
24
|
-
spec.add_development_dependency "rake"
|
25
|
-
if ruby_2_2_2_plus
|
26
|
-
spec.add_development_dependency "rack"
|
27
|
-
else
|
28
|
-
spec.add_development_dependency "rack", '< 2'
|
29
|
-
end
|
30
|
-
spec.add_development_dependency "gimme"
|
31
|
-
spec.add_development_dependency "minitest"
|
32
|
-
spec.add_development_dependency "mocha"
|
33
|
-
spec.add_development_dependency "typhoeus"
|
34
|
-
spec.add_development_dependency "timecop"
|
35
|
-
spec.add_development_dependency "faraday"
|
36
|
-
spec.add_development_dependency "excon"
|
37
|
-
spec.add_development_dependency "logger"
|
38
|
-
spec.add_development_dependency "bundler-gem_version_tasks"
|
39
|
-
spec.add_development_dependency "lmdb"
|
40
|
-
spec.add_development_dependency "daybreak"
|
41
|
-
|
42
|
-
if ruby_2_2_2_plus
|
43
|
-
spec.add_dependency "activesupport"
|
44
|
-
else
|
45
|
-
spec.add_dependency "activesupport", '< 5'
|
46
|
-
end
|
47
|
-
spec.add_dependency "moneta"
|
48
|
-
end
|
@@ -1,31 +0,0 @@
|
|
1
|
-
module ActiveSupport
|
2
|
-
module Cache
|
3
|
-
class MemcacheStore
|
4
|
-
def initialize(cache)
|
5
|
-
@cache = cache
|
6
|
-
end
|
7
|
-
|
8
|
-
def read(key, options = {})
|
9
|
-
@cache.get(key, options)
|
10
|
-
rescue Memcached::NotFound
|
11
|
-
nil
|
12
|
-
end
|
13
|
-
|
14
|
-
def increment(key)
|
15
|
-
@cache.incr(key)
|
16
|
-
end
|
17
|
-
|
18
|
-
def write(key, value, options = {})
|
19
|
-
if expires_in = options.delete(:expires_in)
|
20
|
-
options[:expiry] = expires_in.to_i
|
21
|
-
end
|
22
|
-
|
23
|
-
@cache.set(key, value, options)
|
24
|
-
end
|
25
|
-
|
26
|
-
def delete(key)
|
27
|
-
@cache.delete(key)
|
28
|
-
end
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|
data/lib/circuitbox/notifier.rb
DELETED
@@ -1,34 +0,0 @@
|
|
1
|
-
class Circuitbox
|
2
|
-
class Notifier
|
3
|
-
def initialize(service, partition=nil)
|
4
|
-
@service = service
|
5
|
-
@partition = partition
|
6
|
-
end
|
7
|
-
|
8
|
-
def notify(event)
|
9
|
-
return unless notification_available?
|
10
|
-
ActiveSupport::Notifications.instrument("circuit_#{event}", circuit: circuit_name)
|
11
|
-
end
|
12
|
-
|
13
|
-
def notify_warning(message)
|
14
|
-
return unless notification_available?
|
15
|
-
ActiveSupport::Notifications.instrument("circuit_warning", { circuit: circuit_name, message: message})
|
16
|
-
end
|
17
|
-
|
18
|
-
def metric_gauge(gauge, value)
|
19
|
-
return unless notification_available?
|
20
|
-
ActiveSupport::Notifications.instrument("circuit_gauge", { circuit: circuit_name, gauge: gauge.to_s, value: value })
|
21
|
-
end
|
22
|
-
|
23
|
-
private
|
24
|
-
def circuit_name
|
25
|
-
circuit_name = @service.to_s
|
26
|
-
circuit_name += ":#{@partition}" if @partition
|
27
|
-
circuit_name
|
28
|
-
end
|
29
|
-
|
30
|
-
def notification_available?
|
31
|
-
defined? ActiveSupport::Notifications
|
32
|
-
end
|
33
|
-
end
|
34
|
-
end
|
@@ -1,428 +0,0 @@
|
|
1
|
-
require 'test_helper'
|
2
|
-
|
3
|
-
class CircuitBreakerTest < Minitest::Test
|
4
|
-
class ConnectionError < StandardError; end;
|
5
|
-
|
6
|
-
def setup
|
7
|
-
Circuitbox::CircuitBreaker.reset
|
8
|
-
end
|
9
|
-
|
10
|
-
def test_sleep_window_is_forced_to_equal_time_window
|
11
|
-
circuit = Circuitbox::CircuitBreaker.new(:yammer, sleep_window: 1, time_window: 10)
|
12
|
-
assert_equal circuit.option_value(:sleep_window), circuit.option_value(:time_window)
|
13
|
-
end
|
14
|
-
|
15
|
-
def test_goes_into_half_open_state_on_sleep
|
16
|
-
circuit = Circuitbox::CircuitBreaker.new(:yammer)
|
17
|
-
circuit.send(:open!)
|
18
|
-
assert circuit.send(:half_open?)
|
19
|
-
end
|
20
|
-
|
21
|
-
class Ratio < Minitest::Test
|
22
|
-
def setup
|
23
|
-
Circuitbox::CircuitBreaker.reset
|
24
|
-
@circuit = Circuitbox::CircuitBreaker.new(:yammer,
|
25
|
-
sleep_window: 300,
|
26
|
-
volume_threshold: 5,
|
27
|
-
error_threshold: 33,
|
28
|
-
timeout_seconds: 1)
|
29
|
-
end
|
30
|
-
|
31
|
-
def test_open_circuit_on_100_percent_failure
|
32
|
-
run_counter = 0
|
33
|
-
10.times do
|
34
|
-
@circuit.run do
|
35
|
-
run_counter += 1
|
36
|
-
raise Timeout::Error
|
37
|
-
end
|
38
|
-
end
|
39
|
-
assert_equal 6, run_counter, 'the circuit did not open after 6 failures (5 failures + 10%)'
|
40
|
-
end
|
41
|
-
|
42
|
-
def test_keep_circuit_closed_on_success
|
43
|
-
run_counter = 0
|
44
|
-
10.times do
|
45
|
-
@circuit.run do
|
46
|
-
run_counter += 1
|
47
|
-
'sucess'
|
48
|
-
end
|
49
|
-
end
|
50
|
-
assert_equal 10, run_counter, 'run block was not executed 10 times'
|
51
|
-
end
|
52
|
-
|
53
|
-
def test_open_circuit_on_low_success_rate_below_limit
|
54
|
-
run_counter = 0
|
55
|
-
5.times do
|
56
|
-
@circuit.run do
|
57
|
-
run_counter += 1
|
58
|
-
raise Timeout::Error
|
59
|
-
end
|
60
|
-
end
|
61
|
-
|
62
|
-
# one success
|
63
|
-
@circuit.run { 'success'}
|
64
|
-
assert_equal 5, @circuit.failure_count, 'the total count of failures is not 5'
|
65
|
-
|
66
|
-
5.times do
|
67
|
-
@circuit.run do
|
68
|
-
run_counter += 1
|
69
|
-
raise Timeout::Error
|
70
|
-
end
|
71
|
-
end
|
72
|
-
assert_equal 5, run_counter, 'the circuit did not open after 5 failures (5 failures + 10%)'
|
73
|
-
end
|
74
|
-
|
75
|
-
def test_keep_circuit_closed_on_low_failure_rate_below_failure_limit
|
76
|
-
run_counter = 0
|
77
|
-
7.times do
|
78
|
-
@circuit.run do
|
79
|
-
run_counter += 1
|
80
|
-
'sucess'
|
81
|
-
end
|
82
|
-
end
|
83
|
-
assert_equal 0, @circuit.failure_count, 'some errors were counted'
|
84
|
-
|
85
|
-
3.times do
|
86
|
-
@circuit.run do
|
87
|
-
run_counter += 1
|
88
|
-
raise Timeout::Error
|
89
|
-
end
|
90
|
-
end
|
91
|
-
assert_equal 10, run_counter, 'block was not executed 10 times'
|
92
|
-
assert @circuit.error_rate < 33, 'error_rate pass over 33%'
|
93
|
-
end
|
94
|
-
|
95
|
-
def test_open_circuit_on_high_failure_rate_exceeding_failure_limit
|
96
|
-
run_counter = 0
|
97
|
-
10.times do
|
98
|
-
@circuit.run do
|
99
|
-
run_counter += 1
|
100
|
-
'sucess'
|
101
|
-
end
|
102
|
-
end
|
103
|
-
assert_equal 0, @circuit.failure_count, 'some errors were counted'
|
104
|
-
|
105
|
-
10.times do
|
106
|
-
@circuit.run do
|
107
|
-
run_counter += 1
|
108
|
-
raise Timeout::Error
|
109
|
-
end
|
110
|
-
end
|
111
|
-
# 5 failure on 15 run is 33%
|
112
|
-
assert_equal 15, run_counter, 'block was not executed 10 times'
|
113
|
-
assert @circuit.error_rate >= 33, 'error_rate pass over 33%'
|
114
|
-
end
|
115
|
-
end
|
116
|
-
|
117
|
-
class Exceptions < Minitest::Test
|
118
|
-
class SentinalError < StandardError; end
|
119
|
-
|
120
|
-
def setup
|
121
|
-
Circuitbox::CircuitBreaker.reset
|
122
|
-
@circuit = Circuitbox::CircuitBreaker.new(:yammer, exceptions: [SentinalError])
|
123
|
-
end
|
124
|
-
|
125
|
-
def test_raises_when_circuit_is_open
|
126
|
-
@circuit.stubs(open_flag?: true)
|
127
|
-
assert_raises(Circuitbox::OpenCircuitError) { @circuit.run! {} }
|
128
|
-
end
|
129
|
-
|
130
|
-
def test_raises_on_service_failure
|
131
|
-
assert_raises(Circuitbox::ServiceFailureError) { @circuit.run! { raise SentinalError } }
|
132
|
-
end
|
133
|
-
|
134
|
-
def test_sets_original_error_on_service_failure
|
135
|
-
@circuit.run! { raise SentinalError }
|
136
|
-
rescue Circuitbox::ServiceFailureError => service_failure_error
|
137
|
-
assert_instance_of SentinalError, service_failure_error.original
|
138
|
-
end
|
139
|
-
end
|
140
|
-
|
141
|
-
class CloseAfterSleep < Minitest::Test
|
142
|
-
def setup
|
143
|
-
Circuitbox::CircuitBreaker.reset
|
144
|
-
@circuit = Circuitbox::CircuitBreaker.new(:yammer,
|
145
|
-
sleep_window: 1,
|
146
|
-
time_window: 2,
|
147
|
-
volume_threshold: 5,
|
148
|
-
error_threshold: 5,
|
149
|
-
timeout_seconds: 1)
|
150
|
-
end
|
151
|
-
|
152
|
-
def test_circuit_closes_after_sleep_time_window
|
153
|
-
open_circuit!
|
154
|
-
run_count = 0
|
155
|
-
@circuit.run { run_count += 1 }
|
156
|
-
assert_equal 0, run_count, 'circuit has not opened prior'
|
157
|
-
# it is + 2 on purpose, because + 1 is flaky here
|
158
|
-
sleep @circuit.option_value(:sleep_window) + 2
|
159
|
-
|
160
|
-
@circuit.run { run_count += 1 }
|
161
|
-
assert_equal 1, run_count, 'circuit did not close after sleep'
|
162
|
-
end
|
163
|
-
|
164
|
-
def open_circuit!
|
165
|
-
(@circuit.option_value(:error_threshold) + 1).times { @circuit.run { raise Timeout::Error } }
|
166
|
-
end
|
167
|
-
end
|
168
|
-
|
169
|
-
class HalfOpenState < Minitest::Test
|
170
|
-
def setup
|
171
|
-
Circuitbox::CircuitBreaker.reset
|
172
|
-
@circuit = Circuitbox::CircuitBreaker.new(:yammer)
|
173
|
-
end
|
174
|
-
|
175
|
-
def test_when_in_half_open_state_circuit_opens_on_failure
|
176
|
-
@circuit.stubs(half_open?: true)
|
177
|
-
@circuit.expects(:open!)
|
178
|
-
@circuit.run { raise Timeout::Error }
|
179
|
-
end
|
180
|
-
|
181
|
-
def test_when_in_half_open_state_circuit_closes_on_success
|
182
|
-
@circuit.send(:half_open!)
|
183
|
-
@circuit.run { 'success' }
|
184
|
-
refute @circuit.send(:half_open?)
|
185
|
-
refute @circuit.send(:open?)
|
186
|
-
end
|
187
|
-
end
|
188
|
-
|
189
|
-
def test_should_use_timeout_class_if_exceptions_are_not_defined
|
190
|
-
circuit = Circuitbox::CircuitBreaker.new(:yammer, timeout_seconds: 45)
|
191
|
-
circuit.expects(:timeout).with(45).once
|
192
|
-
emulate_circuit_run(circuit, :success, StandardError)
|
193
|
-
end
|
194
|
-
|
195
|
-
def test_should_not_use_timeout_class_if_custom_exceptions_are_defined
|
196
|
-
circuit = Circuitbox::CircuitBreaker.new(:yammer, exceptions: [ConnectionError])
|
197
|
-
circuit.expects(:timeout).never
|
198
|
-
emulate_circuit_run(circuit, :success, StandardError)
|
199
|
-
end
|
200
|
-
|
201
|
-
def test_should_return_response_if_it_doesnt_timeout
|
202
|
-
circuit = Circuitbox::CircuitBreaker.new(:yammer)
|
203
|
-
response = emulate_circuit_run(circuit, :success, "success")
|
204
|
-
assert_equal "success", response
|
205
|
-
end
|
206
|
-
|
207
|
-
def test_timeout_seconds_run_options_overrides_circuit_options
|
208
|
-
circuit = Circuitbox::CircuitBreaker.new(:yammer, timeout_seconds: 60)
|
209
|
-
circuit.expects(:timeout).with(30).once
|
210
|
-
circuit.run(timeout_seconds: 30) { true }
|
211
|
-
end
|
212
|
-
|
213
|
-
def test_catches_connection_error_failures_if_defined
|
214
|
-
circuit = Circuitbox::CircuitBreaker.new(:yammer, :exceptions => [ConnectionError])
|
215
|
-
response = emulate_circuit_run(circuit, :failure, ConnectionError)
|
216
|
-
assert_equal nil, response
|
217
|
-
end
|
218
|
-
|
219
|
-
def test_doesnt_catch_out_of_scope_exceptions
|
220
|
-
sentinal = Class.new(StandardError)
|
221
|
-
circuit = Circuitbox::CircuitBreaker.new(:yammer, :exceptions => [ConnectionError, Timeout::Error])
|
222
|
-
|
223
|
-
assert_raises(sentinal) do
|
224
|
-
emulate_circuit_run(circuit, :failure, sentinal)
|
225
|
-
end
|
226
|
-
end
|
227
|
-
|
228
|
-
def test_records_response_failure
|
229
|
-
circuit = Circuitbox::CircuitBreaker.new(:yammer, :exceptions => [Timeout::Error])
|
230
|
-
circuit.expects(:log_event).with(:failure)
|
231
|
-
emulate_circuit_run(circuit, :failure, Timeout::Error)
|
232
|
-
end
|
233
|
-
|
234
|
-
def test_records_response_success
|
235
|
-
circuit = Circuitbox::CircuitBreaker.new(:yammer)
|
236
|
-
circuit.expects(:log_event).with(:success)
|
237
|
-
emulate_circuit_run(circuit, :success, "success")
|
238
|
-
end
|
239
|
-
|
240
|
-
def test_does_not_send_request_if_circuit_is_open
|
241
|
-
circuit = Circuitbox::CircuitBreaker.new(:yammer)
|
242
|
-
circuit.stubs(:open? => true)
|
243
|
-
circuit.expects(:yield).never
|
244
|
-
response = emulate_circuit_run(circuit, :failure, Timeout::Error)
|
245
|
-
assert_equal nil, response
|
246
|
-
end
|
247
|
-
|
248
|
-
def test_returns_nil_response_on_failed_request
|
249
|
-
circuit = Circuitbox::CircuitBreaker.new(:yammer)
|
250
|
-
response = emulate_circuit_run(circuit, :failure, Timeout::Error)
|
251
|
-
assert_equal nil, response
|
252
|
-
end
|
253
|
-
|
254
|
-
def test_puts_circuit_to_sleep_once_opened
|
255
|
-
circuit = Circuitbox::CircuitBreaker.new(:yammer)
|
256
|
-
circuit.stubs(:open? => true)
|
257
|
-
|
258
|
-
assert !circuit.send(:open_flag?)
|
259
|
-
emulate_circuit_run(circuit, :failure, Timeout::Error)
|
260
|
-
assert circuit.send(:open_flag?)
|
261
|
-
|
262
|
-
circuit.expects(:open!).never
|
263
|
-
emulate_circuit_run(circuit, :failure, Timeout::Error)
|
264
|
-
end
|
265
|
-
|
266
|
-
def test_open_is_true_if_open_flag
|
267
|
-
circuit = Circuitbox::CircuitBreaker.new(:yammer)
|
268
|
-
circuit.stubs(:open_flag? => true)
|
269
|
-
assert circuit.open?
|
270
|
-
end
|
271
|
-
|
272
|
-
def test_open_checks_if_volume_threshold_has_passed
|
273
|
-
circuit = Circuitbox::CircuitBreaker.new(:yammer)
|
274
|
-
circuit.stubs(:open_flag? => false)
|
275
|
-
|
276
|
-
circuit.expects(:passed_volume_threshold?).once
|
277
|
-
circuit.open?
|
278
|
-
end
|
279
|
-
|
280
|
-
def test_open_checks_error_rate_threshold
|
281
|
-
circuit = Circuitbox::CircuitBreaker.new(:yammer)
|
282
|
-
circuit.stubs(:open_flag? => false,
|
283
|
-
:passed_volume_threshold? => true)
|
284
|
-
|
285
|
-
circuit.expects(:passed_rate_threshold?).once
|
286
|
-
circuit.open?
|
287
|
-
end
|
288
|
-
|
289
|
-
def test_open_is_false_if_awake_and_under_rate_threshold
|
290
|
-
circuit = Circuitbox::CircuitBreaker.new(:yammer)
|
291
|
-
circuit.stubs(:open_flag? => false,
|
292
|
-
:passed_volume_threshold? => false,
|
293
|
-
:passed_rate_threshold => false)
|
294
|
-
|
295
|
-
assert !circuit.open?
|
296
|
-
end
|
297
|
-
|
298
|
-
def test_error_rate_threshold_calculation
|
299
|
-
circuit = Circuitbox::CircuitBreaker.new(:yammer)
|
300
|
-
circuit.stubs(:failure_count => 3, :success_count => 2)
|
301
|
-
assert circuit.send(:passed_rate_threshold?)
|
302
|
-
|
303
|
-
circuit.stubs(:failure_count => 2, :success_count => 3)
|
304
|
-
assert !circuit.send(:passed_rate_threshold?)
|
305
|
-
end
|
306
|
-
|
307
|
-
def test_logs_and_retrieves_success_events
|
308
|
-
circuit = Circuitbox::CircuitBreaker.new(:yammer)
|
309
|
-
5.times { circuit.send(:log_event, :success) }
|
310
|
-
assert_equal 5, circuit.send(:success_count)
|
311
|
-
end
|
312
|
-
|
313
|
-
def test_logs_and_retrieves_failure_events
|
314
|
-
circuit = Circuitbox::CircuitBreaker.new(:yammer)
|
315
|
-
5.times { circuit.send(:log_event, :failure) }
|
316
|
-
assert_equal 5, circuit.send(:failure_count)
|
317
|
-
end
|
318
|
-
|
319
|
-
def test_logs_events_by_minute
|
320
|
-
circuit = Circuitbox::CircuitBreaker.new(:yammer)
|
321
|
-
|
322
|
-
Timecop.travel(Time.now.change(sec: 5))
|
323
|
-
4.times { circuit.send(:log_event, :success) }
|
324
|
-
assert_equal 4, circuit.send(:success_count)
|
325
|
-
|
326
|
-
Timecop.travel(1.minute.from_now)
|
327
|
-
7.times { circuit.send(:log_event, :success) }
|
328
|
-
assert_equal 7, circuit.send(:success_count)
|
329
|
-
|
330
|
-
Timecop.travel(30.seconds.from_now)
|
331
|
-
circuit.send(:log_event, :success)
|
332
|
-
assert_equal 8, circuit.send(:success_count)
|
333
|
-
|
334
|
-
Timecop.travel(50.seconds.from_now)
|
335
|
-
assert_equal 0, circuit.send(:success_count)
|
336
|
-
end
|
337
|
-
|
338
|
-
class Notifications < Minitest::Test
|
339
|
-
def setup
|
340
|
-
Circuitbox::CircuitBreaker.reset
|
341
|
-
end
|
342
|
-
|
343
|
-
def test_notification_on_open
|
344
|
-
notifier = gimme_notifier
|
345
|
-
circuit = Circuitbox::CircuitBreaker.new(:yammer, notifier_class: notifier)
|
346
|
-
10.times { circuit.run { raise Timeout::Error }}
|
347
|
-
assert notifier.notified?, 'no notification sent'
|
348
|
-
end
|
349
|
-
|
350
|
-
def test_notification_on_close
|
351
|
-
notifier = gimme_notifier
|
352
|
-
circuit = Circuitbox::CircuitBreaker.new(:yammer, notifier_class: notifier)
|
353
|
-
5.times { circuit.run { raise Timeout::Error }}
|
354
|
-
notifier.clear_notified!
|
355
|
-
10.times { circuit.run { 'success' }}
|
356
|
-
assert notifier.notified?, 'no notification sent'
|
357
|
-
end
|
358
|
-
|
359
|
-
def test_warning_when_sleep_window_is_shorter_than_time_window
|
360
|
-
notifier = gimme_notifier
|
361
|
-
Circuitbox::CircuitBreaker.new(:yammer,
|
362
|
-
notifier_class: notifier,
|
363
|
-
sleep_window: 1,
|
364
|
-
time_window: 10)
|
365
|
-
assert notifier.notified?, 'no notification sent'
|
366
|
-
end
|
367
|
-
|
368
|
-
def test_does_not_warn_on_sleep_window_being_correctly_sized
|
369
|
-
notifier = gimme_notifier
|
370
|
-
Circuitbox::CircuitBreaker.new(:yammer,
|
371
|
-
notifier_class: notifier,
|
372
|
-
sleep_window: 11,
|
373
|
-
time_window: 10)
|
374
|
-
assert_equal false, notifier.notified?, 'no notification sent'
|
375
|
-
end
|
376
|
-
|
377
|
-
def test_notifies_on_success_rate_calculation
|
378
|
-
notifier = gimme_notifier(metric: :error_rate, metric_value: 0.0)
|
379
|
-
circuit = Circuitbox::CircuitBreaker.new(:yammer, notifier_class: notifier)
|
380
|
-
10.times { circuit.run { "success" } }
|
381
|
-
assert notifier.notified?, "no notification sent"
|
382
|
-
end
|
383
|
-
|
384
|
-
def test_notifies_on_error_rate_calculation
|
385
|
-
notifier = gimme_notifier(metric: :failure_count, metric_value: 1)
|
386
|
-
circuit = Circuitbox::CircuitBreaker.new(:yammer, notifier_class: notifier)
|
387
|
-
10.times { circuit.run { raise Timeout::Error }}
|
388
|
-
assert notifier.notified?, 'no notification sent'
|
389
|
-
end
|
390
|
-
|
391
|
-
def test_success_count_on_error_rate_calculation
|
392
|
-
notifier = gimme_notifier(metric: :success_count, metric_value: 6)
|
393
|
-
circuit = Circuitbox::CircuitBreaker.new(:yammer, notifier_class: notifier)
|
394
|
-
10.times { circuit.run { 'success' }}
|
395
|
-
assert notifier.notified?, 'no notification sent'
|
396
|
-
end
|
397
|
-
|
398
|
-
def gimme_notifier(opts={})
|
399
|
-
metric = opts.fetch(:metric,:error_rate)
|
400
|
-
metric_value = opts.fetch(:metric_value, 0.0)
|
401
|
-
warning_msg = opts.fetch(:warning_msg, '')
|
402
|
-
fake_notifier = gimme
|
403
|
-
notified = false
|
404
|
-
give(fake_notifier).notify(:open) { notified = true }
|
405
|
-
give(fake_notifier).notify(:close) { notified = true }
|
406
|
-
give(fake_notifier).notify_warning(Gimme::Matchers::Anything.new) { notified = true }
|
407
|
-
give(fake_notifier).metric_gauge(metric, metric_value) { notified = true }
|
408
|
-
fake_notifier_class = gimme
|
409
|
-
give(fake_notifier_class).new(:yammer,nil) { fake_notifier }
|
410
|
-
give(fake_notifier_class).notified? { notified }
|
411
|
-
give(fake_notifier_class).clear_notified! { notified = false }
|
412
|
-
fake_notifier_class
|
413
|
-
end
|
414
|
-
end
|
415
|
-
|
416
|
-
def emulate_circuit_run(circuit, response_type, response_value)
|
417
|
-
circuit.run do
|
418
|
-
case response_type
|
419
|
-
when :failure
|
420
|
-
raise response_value
|
421
|
-
when :success
|
422
|
-
response_value
|
423
|
-
end
|
424
|
-
end
|
425
|
-
rescue Timeout::Error
|
426
|
-
nil
|
427
|
-
end
|
428
|
-
end
|