circuitbox 0.10.4 → 0.11.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +2 -0
- data/.ruby-version +1 -0
- data/.travis.yml +2 -0
- data/Gemfile +1 -0
- data/README.md +87 -0
- data/Rakefile +8 -0
- data/benchmark/circuit_store_benchmark.rb +116 -0
- data/circuitbox.gemspec +4 -0
- data/lib/circuitbox/circuit_breaker.rb +13 -12
- data/lib/circuitbox/excon_middleware.rb +111 -0
- data/lib/circuitbox/version.rb +1 -1
- data/lib/circuitbox.rb +4 -2
- data/test/excon_middleware_test.rb +131 -0
- data/test/integration/circuitbox_cross_process_open_test.rb +55 -0
- data/test/integration_helper.rb +3 -3
- metadata +65 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d0fe08dafcf12318b2880700034caf98461a2f3e
|
4
|
+
data.tar.gz: f136784e4d4d32ffa0b53580fb64eded3bf4d5ba
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0e2870b5f3b3cffd2623f04937be7794fe2ee0085655b65b2533da3ba421e9e7772c4b85593097f300aa4f0984b484f590a8811eb22b667a31e7a8d81c9070d2
|
7
|
+
data.tar.gz: c98b3b8e64b4def135717ae7eedb833807d4a484cb6772d284c38c08cb992013a8adc64041ffaddb529fdfecda187287160216467940b63c252a5d3ce765bcb7
|
data/.gitignore
CHANGED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.1.5
|
data/.travis.yml
CHANGED
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -57,6 +57,10 @@ class ExampleServiceClient
|
|
57
57
|
# number of requests within 1 minute before it calculates error rates
|
58
58
|
volume_threshold: 10,
|
59
59
|
|
60
|
+
# the store you want to use to save the circuit state so it can be
|
61
|
+
# tracked, this needs to be Moneta compatible, and support increment
|
62
|
+
cache: Moneta.new(:Memory)
|
63
|
+
|
60
64
|
# exceeding this rate will open the circuit
|
61
65
|
error_threshold: 50,
|
62
66
|
|
@@ -75,6 +79,19 @@ Circuitbox.circuit(:yammer, {
|
|
75
79
|
})
|
76
80
|
```
|
77
81
|
|
82
|
+
## Circuit Store (:cache)
|
83
|
+
|
84
|
+
Holds all the relevant data to trip the circuit if a given number of requests
|
85
|
+
fail in a specified period of time. The store is based on
|
86
|
+
[Moneta](https://github.com/minad/moneta) so there are a lot of stores to choose
|
87
|
+
from. There are some pre-requisits they need to satisfy so:
|
88
|
+
|
89
|
+
- Need to support increment, this is true for most but not all available stores.
|
90
|
+
- Need to support concurrent access if you share them. For example sharing a
|
91
|
+
KyotoCabinet store across process fails because the store is single writer
|
92
|
+
multiple readers, and all circuits sharing the store need to be able to write.
|
93
|
+
|
94
|
+
|
78
95
|
## Monitoring & Statistics
|
79
96
|
|
80
97
|
You can also run `rake circuits:stats SERVICE={service_name}` to see successes, failures and opened circuits.
|
@@ -136,6 +153,74 @@ end
|
|
136
153
|
|
137
154
|
```
|
138
155
|
|
156
|
+
### Multi process Circuits
|
157
|
+
|
158
|
+
`circuit_store` is backed by [Moneta](https://github.com/minad/moneta) which
|
159
|
+
supports multiple backends. This can be configured by passing `cache:
|
160
|
+
Moneta.new(:PStore, file: "myfile.store")` to use for example the built in
|
161
|
+
PStore ruby library for persisted store, which can be shared cross process.
|
162
|
+
|
163
|
+
Depending on your requirements different stores can make sense, see the
|
164
|
+
benchmarks and [moneta
|
165
|
+
feature](https://github.com/minad/moneta#backend-feature-matrix) matrix for
|
166
|
+
details.
|
167
|
+
|
168
|
+
```
|
169
|
+
user system total real
|
170
|
+
memory: 1.440000 0.140000 1.580000 ( 1.579244)
|
171
|
+
lmdb: 4.330000 3.280000 7.610000 ( 13.086398)
|
172
|
+
pstore: 23.680000 4.350000 28.030000 ( 28.094312)
|
173
|
+
daybreak: 2.270000 0.450000 2.720000 ( 2.626748)
|
174
|
+
```
|
175
|
+
|
176
|
+
You can run the benchmarks yourself by running `rake benchmark`.
|
177
|
+
|
178
|
+
### Memory
|
179
|
+
|
180
|
+
An in memory store, which is local to the process. This is not threadsafe so it
|
181
|
+
is not useable with multithreaded webservers for example. It is always going to
|
182
|
+
be the fastest option if no multi-process or thread is required, like in
|
183
|
+
development on Webbrick.
|
184
|
+
|
185
|
+
This is the default.
|
186
|
+
|
187
|
+
```ruby
|
188
|
+
Circuitbox.circuit :identifier, cache: Moneta.new(:Memory)
|
189
|
+
```
|
190
|
+
|
191
|
+
### LMDB
|
192
|
+
|
193
|
+
An persisted directory backed store, which is thread and multi process save.
|
194
|
+
depends on the `lmdb` gem. It is slower than Memory or Daybreak, but can be
|
195
|
+
used in multi thread and multi process environments like like Puma.
|
196
|
+
|
197
|
+
```ruby
|
198
|
+
require "lmdb"
|
199
|
+
Circuitbox.circuit :identifier, cache: Moneta.new(:LMDB, dir: "./", db: "mydb")
|
200
|
+
```
|
201
|
+
|
202
|
+
### PStore
|
203
|
+
|
204
|
+
An persisted file backed store, which comes with the ruby
|
205
|
+
[stdlib](http://ruby-doc.org/stdlib-2.3.0/libdoc/pstore/rdoc/PStore.html). It
|
206
|
+
has no external dependecies and works on every ruby implementation. Due to it
|
207
|
+
being file backed it is multi process save, good for development using Unicorn.
|
208
|
+
|
209
|
+
```ruby
|
210
|
+
Circuitbox.circuit :identifier, cache: Moneta.new(:PStore, file: "db.pstore")
|
211
|
+
```
|
212
|
+
|
213
|
+
### Daybreak
|
214
|
+
|
215
|
+
Persisted, file backed key value store in pure ruby. It is process save and
|
216
|
+
outperforms most other stores in circuitbox. This is recommended for production
|
217
|
+
use with Unicorn. It depends on the `daybreak` gem.
|
218
|
+
|
219
|
+
```ruby
|
220
|
+
require "daybreak"
|
221
|
+
Circuitbox.circuit :identifier, cache: Moneta.new(:Daybreak, file: "db.daybreak")
|
222
|
+
```
|
223
|
+
|
139
224
|
## Faraday
|
140
225
|
|
141
226
|
Circuitbox ships with [Faraday HTTP client](https://github.com/lostisland/faraday) middleware.
|
@@ -212,6 +297,8 @@ c.use Circuitbox::FaradayMiddleware, open_circuit: lambda { |response| response.
|
|
212
297
|
## CHANGELOG
|
213
298
|
|
214
299
|
### version next
|
300
|
+
- fix URI require missing (https://github.com/yammer/circuitbox/pull/42 @gottfrois)
|
301
|
+
- configurable circuitbox store backend via Moneta supporting multi process circuits
|
215
302
|
|
216
303
|
### v0.10.4
|
217
304
|
- Issue #39, keep the original backtrace for the wrapped exception around when
|
data/Rakefile
CHANGED
@@ -6,5 +6,13 @@ Rake::TestTask.new do |t|
|
|
6
6
|
t.test_files = FileList['test/**/*_test.rb']
|
7
7
|
end
|
8
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
|
+
|
9
17
|
desc "Run tests"
|
10
18
|
task :default => :test
|
@@ -0,0 +1,116 @@
|
|
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
|
+
before = circuit.open?
|
31
|
+
circuit.run { service }
|
32
|
+
after = circuit.open?
|
33
|
+
circuit.try_close_next_time if circuit.open?
|
34
|
+
end
|
35
|
+
|
36
|
+
def without_gc
|
37
|
+
GC.start
|
38
|
+
GC.disable
|
39
|
+
yield
|
40
|
+
GC.enable
|
41
|
+
end
|
42
|
+
|
43
|
+
def benchmark_circuitbox_method_with_reporter method, reporter
|
44
|
+
without_gc { send(method, reporter) }
|
45
|
+
Circuitbox.reset
|
46
|
+
end
|
47
|
+
|
48
|
+
def circuit_with_cache cache
|
49
|
+
Circuitbox.circuit :performance, CIRCUIT_OPTIONS.merge(cache: cache)
|
50
|
+
end
|
51
|
+
|
52
|
+
CIRCUIT_OPTIONS = {
|
53
|
+
exceptions: [RuntimeError],
|
54
|
+
sleep_window: 0,
|
55
|
+
time_window: 1
|
56
|
+
}
|
57
|
+
|
58
|
+
RUNS = 10000
|
59
|
+
|
60
|
+
def circuit_store_memory_one_process reporter
|
61
|
+
circuit = circuit_with_cache Moneta.new(:Memory)
|
62
|
+
|
63
|
+
reporter.report "memory:" do
|
64
|
+
RUNS.times { run_flip_flopping circuit }
|
65
|
+
end
|
66
|
+
|
67
|
+
circuit.circuit_store.close
|
68
|
+
end
|
69
|
+
|
70
|
+
def circuit_store_pstore_one_process reporter
|
71
|
+
Tempfile.create("test_circuit_store_pstore_one_process") do |dbfile|
|
72
|
+
circuit = circuit_with_cache Moneta.new(:PStore, file: dbfile)
|
73
|
+
|
74
|
+
reporter.report "pstore:" do
|
75
|
+
RUNS.times { run_flip_flopping circuit }
|
76
|
+
end
|
77
|
+
|
78
|
+
circuit.circuit_store.close
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def circuit_store_lmdb_one_process reporter
|
83
|
+
Dir.mktmpdir("test_circuit_store_lmdb_one_process") do |dbdir|
|
84
|
+
circuit = circuit_with_cache Moneta.new(:LMDB, dir: dbdir, db: "circuitbox_lmdb")
|
85
|
+
|
86
|
+
reporter.report "lmdb:" do
|
87
|
+
RUNS.times { run_flip_flopping circuit }
|
88
|
+
end
|
89
|
+
|
90
|
+
circuit.circuit_store.close
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def circuit_store_daybreak_one_process reporter
|
95
|
+
Tempfile.create("test_circuit_store_daybreak_one_process") do |dbfile|
|
96
|
+
circuit = circuit_with_cache Moneta.new(:Daybreak, file: dbfile)
|
97
|
+
|
98
|
+
reporter.report "daybreak:" do
|
99
|
+
RUNS.times { run_flip_flopping circuit }
|
100
|
+
end
|
101
|
+
|
102
|
+
circuit.circuit_store.close
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
Benchmark.bm(8) do |x|
|
107
|
+
benchmark_circuitbox_method_with_reporter :circuit_store_memory_one_process, x
|
108
|
+
benchmark_circuitbox_method_with_reporter :circuit_store_lmdb_one_process, x
|
109
|
+
benchmark_circuitbox_method_with_reporter :circuit_store_pstore_one_process, x
|
110
|
+
benchmark_circuitbox_method_with_reporter :circuit_store_daybreak_one_process, x
|
111
|
+
end
|
112
|
+
|
113
|
+
|
114
|
+
|
115
|
+
|
116
|
+
|
data/circuitbox.gemspec
CHANGED
@@ -27,8 +27,12 @@ Gem::Specification.new do |spec|
|
|
27
27
|
spec.add_development_dependency "typhoeus"
|
28
28
|
spec.add_development_dependency "timecop"
|
29
29
|
spec.add_development_dependency "faraday"
|
30
|
+
spec.add_development_dependency "excon"
|
30
31
|
spec.add_development_dependency "logger"
|
31
32
|
spec.add_development_dependency "bundler-gem_version_tasks"
|
33
|
+
spec.add_development_dependency "lmdb"
|
34
|
+
spec.add_development_dependency "daybreak"
|
32
35
|
|
33
36
|
spec.add_dependency "activesupport"
|
37
|
+
spec.add_dependency "moneta"
|
34
38
|
end
|
@@ -118,11 +118,11 @@ class Circuitbox
|
|
118
118
|
end
|
119
119
|
|
120
120
|
def failure_count
|
121
|
-
circuit_store.
|
121
|
+
circuit_store.load(stat_storage_key(:failure), raw: true).to_i
|
122
122
|
end
|
123
123
|
|
124
124
|
def success_count
|
125
|
-
circuit_store.
|
125
|
+
circuit_store.load(stat_storage_key(:success), raw: true).to_i
|
126
126
|
end
|
127
127
|
|
128
128
|
def try_close_next_time
|
@@ -133,7 +133,7 @@ class Circuitbox
|
|
133
133
|
def open!
|
134
134
|
log_event :open
|
135
135
|
logger.debug "[CIRCUIT] opening #{service} circuit"
|
136
|
-
circuit_store.
|
136
|
+
circuit_store.store(storage_key(:asleep), true, expires_in: option_value(:sleep_window).seconds)
|
137
137
|
half_open!
|
138
138
|
was_open!
|
139
139
|
end
|
@@ -145,24 +145,24 @@ class Circuitbox
|
|
145
145
|
end
|
146
146
|
|
147
147
|
def was_open!
|
148
|
-
circuit_store.
|
148
|
+
circuit_store.store(storage_key(:was_open), true)
|
149
149
|
end
|
150
150
|
|
151
151
|
def was_open?
|
152
|
-
circuit_store
|
152
|
+
circuit_store[storage_key(:was_open)].present?
|
153
153
|
end
|
154
154
|
### END
|
155
155
|
|
156
156
|
def half_open!
|
157
|
-
circuit_store.
|
157
|
+
circuit_store.store(storage_key(:half_open), true)
|
158
158
|
end
|
159
159
|
|
160
160
|
def open_flag?
|
161
|
-
circuit_store
|
161
|
+
circuit_store[storage_key(:asleep)].present?
|
162
162
|
end
|
163
163
|
|
164
164
|
def half_open?
|
165
|
-
circuit_store
|
165
|
+
circuit_store[storage_key(:half_open)].present?
|
166
166
|
end
|
167
167
|
|
168
168
|
def passed_volume_threshold?
|
@@ -219,16 +219,17 @@ class Circuitbox
|
|
219
219
|
|
220
220
|
# When there is a successful response within a stat interval, clear the failures.
|
221
221
|
def clear_failures!
|
222
|
-
circuit_store.
|
222
|
+
circuit_store.store(stat_storage_key(:failure), 0, raw: true)
|
223
223
|
end
|
224
224
|
|
225
225
|
# Logs to process memory.
|
226
226
|
def log_event_to_process(event)
|
227
227
|
key = stat_storage_key(event)
|
228
|
-
if circuit_store.
|
228
|
+
if circuit_store.load(key, raw: true)
|
229
229
|
circuit_store.increment(key)
|
230
230
|
else
|
231
|
-
|
231
|
+
# yes we want a string here, as the underlying stores impement this as a native type.
|
232
|
+
circuit_store.store(key, "1", raw: true)
|
232
233
|
end
|
233
234
|
end
|
234
235
|
|
@@ -237,7 +238,7 @@ class Circuitbox
|
|
237
238
|
if stat_store.read(key, raw: true)
|
238
239
|
stat_store.increment(key)
|
239
240
|
else
|
240
|
-
stat_store.
|
241
|
+
stat_store.store(key, 1)
|
241
242
|
end
|
242
243
|
end
|
243
244
|
|
@@ -0,0 +1,111 @@
|
|
1
|
+
require 'excon'
|
2
|
+
require 'circuitbox'
|
3
|
+
|
4
|
+
class Circuitbox
|
5
|
+
class ExconMiddleware < Excon::Middleware::Base
|
6
|
+
class RequestFailed < StandardError; end
|
7
|
+
|
8
|
+
DEFAULT_EXCEPTIONS = [
|
9
|
+
Excon::Errors::Timeout,
|
10
|
+
RequestFailed
|
11
|
+
]
|
12
|
+
|
13
|
+
class NullResponse < Excon::Response
|
14
|
+
def initialize(response, exception)
|
15
|
+
@original_response = response
|
16
|
+
@original_exception = exception
|
17
|
+
super(status: 503, response_headers: {})
|
18
|
+
end
|
19
|
+
|
20
|
+
def []=(key, value)
|
21
|
+
@data[key] = value
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
attr_reader :opts
|
26
|
+
|
27
|
+
def initialize(stack, opts = {})
|
28
|
+
@stack = stack
|
29
|
+
default_options = { open_circuit: lambda { |response| response[:status] >= 400 } }
|
30
|
+
@opts = default_options.merge(opts)
|
31
|
+
super(stack)
|
32
|
+
end
|
33
|
+
|
34
|
+
def error_call(datum)
|
35
|
+
circuit(datum).run!(run_options(datum)) do
|
36
|
+
raise RequestFailed
|
37
|
+
end
|
38
|
+
rescue Circuitbox::Error => exception
|
39
|
+
circuit_open_value(datum, datum[:response], exception)
|
40
|
+
end
|
41
|
+
|
42
|
+
def request_call(datum)
|
43
|
+
circuit(datum).run!(run_options(datum)) do
|
44
|
+
@stack.request_call(datum)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def response_call(datum)
|
49
|
+
circuit(datum).run!(run_options(datum)) do
|
50
|
+
raise RequestFailed if open_circuit?(datum[:response])
|
51
|
+
end
|
52
|
+
@stack.response_call(datum)
|
53
|
+
rescue Circuitbox::Error => exception
|
54
|
+
circuit_open_value(datum, datum[:response], exception)
|
55
|
+
end
|
56
|
+
|
57
|
+
def identifier
|
58
|
+
@identifier ||= opts.fetch(:identifier, ->(env) { env[:path] })
|
59
|
+
end
|
60
|
+
|
61
|
+
def exceptions
|
62
|
+
circuit_breaker_options[:exceptions]
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def circuit(datum)
|
68
|
+
id = identifier.respond_to?(:call) ? identifier.call(datum) : identifier
|
69
|
+
circuitbox.circuit id, circuit_breaker_options
|
70
|
+
end
|
71
|
+
|
72
|
+
def run_options(datum)
|
73
|
+
opts.merge(datum)[:circuit_breaker_run_options] || {}
|
74
|
+
end
|
75
|
+
|
76
|
+
def open_circuit?(response)
|
77
|
+
opts[:open_circuit].call(response)
|
78
|
+
end
|
79
|
+
|
80
|
+
def circuitbox
|
81
|
+
@circuitbox ||= opts.fetch(:circuitbox, Circuitbox)
|
82
|
+
end
|
83
|
+
|
84
|
+
def circuit_open_value(env, response, exception)
|
85
|
+
env[:circuit_breaker_default_value] || default_value.call(response, exception)
|
86
|
+
end
|
87
|
+
|
88
|
+
def circuit_breaker_options
|
89
|
+
return @circuit_breaker_options if @current_adapter
|
90
|
+
|
91
|
+
@circuit_breaker_options = opts.fetch(:circuit_breaker_options, {})
|
92
|
+
@circuit_breaker_options.merge!(
|
93
|
+
exceptions: opts.fetch(:exceptions, DEFAULT_EXCEPTIONS)
|
94
|
+
)
|
95
|
+
end
|
96
|
+
|
97
|
+
def default_value
|
98
|
+
return @default_value if @default_value
|
99
|
+
|
100
|
+
default = opts.fetch(:default_value) do
|
101
|
+
lambda { |response, exception| NullResponse.new(response, exception) }
|
102
|
+
end
|
103
|
+
|
104
|
+
@default_value = if default.respond_to?(:call)
|
105
|
+
default
|
106
|
+
else
|
107
|
+
lambda { |*| default }
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
data/lib/circuitbox/version.rb
CHANGED
data/lib/circuitbox.rb
CHANGED
@@ -1,11 +1,13 @@
|
|
1
|
+
require 'uri'
|
1
2
|
require 'singleton'
|
2
3
|
require 'active_support'
|
3
4
|
require 'logger'
|
4
5
|
require 'timeout'
|
6
|
+
require 'moneta'
|
5
7
|
|
6
8
|
require 'circuitbox/version'
|
7
9
|
require 'circuitbox/memcache_store'
|
8
|
-
require 'circuitbox/railtie' if defined?(Rails)
|
10
|
+
require 'circuitbox/railtie' if defined?(Rails)
|
9
11
|
require 'circuitbox/circuit_breaker'
|
10
12
|
require 'circuitbox/notifier'
|
11
13
|
|
@@ -35,7 +37,7 @@ class Circuitbox
|
|
35
37
|
end
|
36
38
|
|
37
39
|
def self.circuit_store
|
38
|
-
self.instance.circuit_store ||=
|
40
|
+
self.instance.circuit_store ||= Moneta.new(:Memory)
|
39
41
|
end
|
40
42
|
|
41
43
|
def self.circuit_store=(store)
|
@@ -0,0 +1,131 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'circuitbox/excon_middleware'
|
3
|
+
|
4
|
+
class SentialException < StandardError; end
|
5
|
+
|
6
|
+
class Circuitbox
|
7
|
+
class ExconMiddlewareTest < Minitest::Test
|
8
|
+
|
9
|
+
attr_reader :app
|
10
|
+
|
11
|
+
def setup
|
12
|
+
@app = gimme
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_default_identifier
|
16
|
+
env = { path: "sential" }
|
17
|
+
assert_equal "sential", ExconMiddleware.new(app).identifier.call(env)
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_overwrite_identifier
|
21
|
+
middleware = ExconMiddleware.new(app, identifier: "sential")
|
22
|
+
assert_equal middleware.identifier, "sential"
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_overwrite_default_value_generator_lambda
|
26
|
+
stub_circuitbox
|
27
|
+
env = { path: "path" }
|
28
|
+
give(circuitbox).circuit("path", anything) { circuit }
|
29
|
+
give(circuit).run!(anything) { raise Circuitbox::Error }
|
30
|
+
default_value_generator = lambda { |_, _| :sential }
|
31
|
+
middleware = ExconMiddleware.new(app,
|
32
|
+
circuitbox: circuitbox,
|
33
|
+
default_value: default_value_generator)
|
34
|
+
assert_equal :sential, middleware.error_call(env)
|
35
|
+
end
|
36
|
+
|
37
|
+
def test_overwrite_default_value_generator_static_value
|
38
|
+
stub_circuitbox
|
39
|
+
env = { path: "path" }
|
40
|
+
give(circuitbox).circuit("path", anything) { circuit }
|
41
|
+
give(circuit).run!(anything) { raise Circuitbox::Error }
|
42
|
+
middleware = ExconMiddleware.new(app, circuitbox: circuitbox, default_value: :sential)
|
43
|
+
assert_equal :sential, middleware.error_call(env)
|
44
|
+
end
|
45
|
+
|
46
|
+
def test_default_exceptions
|
47
|
+
middleware = ExconMiddleware.new(app)
|
48
|
+
assert_includes middleware.exceptions, Excon::Errors::Timeout
|
49
|
+
assert_includes middleware.exceptions, ExconMiddleware::RequestFailed
|
50
|
+
end
|
51
|
+
|
52
|
+
def test_overridde_success_response
|
53
|
+
env = { path: "path", response: { status: 400 } }
|
54
|
+
error_response = lambda { |r| r[:status] >= 500 }
|
55
|
+
give(app).response_call(anything) { Excon::Response.new(status: 400) }
|
56
|
+
mw = ExconMiddleware.new(app, open_circuit: error_response)
|
57
|
+
response = mw.response_call(env)
|
58
|
+
assert_kind_of Excon::Response, response
|
59
|
+
assert_equal response.status, 400
|
60
|
+
end
|
61
|
+
|
62
|
+
def test_default_success_response
|
63
|
+
env = { path: "path", response: { status: 400 } }
|
64
|
+
app = gimme
|
65
|
+
give(app).response_call(anything) { Excon::Response.new(status: 400) }
|
66
|
+
response = nil
|
67
|
+
|
68
|
+
mw = ExconMiddleware.new(app)
|
69
|
+
response = mw.response_call(env)
|
70
|
+
|
71
|
+
assert_kind_of Excon::Response, response
|
72
|
+
assert_equal response.status, 503
|
73
|
+
end
|
74
|
+
|
75
|
+
def test_overwrite_exceptions
|
76
|
+
middleware = ExconMiddleware.new(app, exceptions: [SentialException])
|
77
|
+
assert_includes middleware.exceptions, SentialException
|
78
|
+
end
|
79
|
+
|
80
|
+
def test_pass_circuit_breaker_run_options
|
81
|
+
stub_circuitbox
|
82
|
+
give(circuit).run!(:sential)
|
83
|
+
give(circuitbox).circuit("path", anything) { circuit }
|
84
|
+
env = { path: "path", circuit_breaker_run_options: :sential }
|
85
|
+
middleware = ExconMiddleware.new(app, circuitbox: circuitbox)
|
86
|
+
middleware.request_call(env)
|
87
|
+
verify(circuit, 1.times).run!(:sential)
|
88
|
+
end
|
89
|
+
|
90
|
+
def test_pass_circuit_breaker_options
|
91
|
+
stub_circuitbox
|
92
|
+
env = { path: "path" }
|
93
|
+
expected_circuit_breaker_options = {
|
94
|
+
sential: :sential,
|
95
|
+
exceptions: ExconMiddleware::DEFAULT_EXCEPTIONS
|
96
|
+
}
|
97
|
+
give(circuitbox).circuit("path", expected_circuit_breaker_options) { circuit }
|
98
|
+
options = { circuitbox: circuitbox, circuit_breaker_options: { sential: :sential } }
|
99
|
+
middleware = ExconMiddleware.new(app, options)
|
100
|
+
middleware.request_call(env)
|
101
|
+
|
102
|
+
verify(circuitbox, 1.times).circuit("path", expected_circuit_breaker_options)
|
103
|
+
end
|
104
|
+
|
105
|
+
def test_overwrite_circuitbreaker_default_value
|
106
|
+
stub_circuitbox
|
107
|
+
env = { path: "path", circuit_breaker_default_value: :sential }
|
108
|
+
give(circuitbox).circuit("path", anything) { circuit }
|
109
|
+
give(circuit).run!(anything) { raise Circuitbox::Error }
|
110
|
+
middleware = ExconMiddleware.new(app, circuitbox: circuitbox)
|
111
|
+
assert_equal middleware.error_call(env), :sential
|
112
|
+
end
|
113
|
+
|
114
|
+
def test_return_null_response_for_open_circuit
|
115
|
+
stub_circuitbox
|
116
|
+
env = { path: "path" }
|
117
|
+
give(circuit).run!(anything) { raise Circuitbox::Error }
|
118
|
+
give(circuitbox).circuit("path", anything) { circuit }
|
119
|
+
mw = ExconMiddleware.new(app, circuitbox: circuitbox)
|
120
|
+
response = mw.error_call(env)
|
121
|
+
assert_kind_of Excon::Response, response
|
122
|
+
assert_equal response.status, 503
|
123
|
+
end
|
124
|
+
|
125
|
+
attr_reader :circuitbox, :circuit
|
126
|
+
def stub_circuitbox
|
127
|
+
@circuitbox = gimme
|
128
|
+
@circuit = gimme
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require "integration_helper"
|
2
|
+
require "tempfile"
|
3
|
+
require "typhoeus/adapters/faraday"
|
4
|
+
require "pstore"
|
5
|
+
|
6
|
+
class Circuitbox
|
7
|
+
|
8
|
+
class CrossProcessTest < Minitest::Test
|
9
|
+
include IntegrationHelpers
|
10
|
+
|
11
|
+
attr_reader :connection, :failure_url, :dbfile
|
12
|
+
|
13
|
+
@@only_once = false
|
14
|
+
def setup
|
15
|
+
if !@@only_once
|
16
|
+
@dbfile = Tempfile.open("circuitbox_test_cross_process")
|
17
|
+
end
|
18
|
+
|
19
|
+
@connection = Faraday.new do |c|
|
20
|
+
c.use FaradayMiddleware, identifier: "circuitbox_test_cross_process",
|
21
|
+
circuit_breaker_options: { cache: Moneta.new(:PStore, file: dbfile) }
|
22
|
+
c.adapter :typhoeus # support in_parallel
|
23
|
+
end
|
24
|
+
@failure_url = "http://localhost:4713"
|
25
|
+
|
26
|
+
if !@@only_once
|
27
|
+
thread = Thread.new do
|
28
|
+
Rack::Handler::WEBrick.run(Proc.new { |env| ["Failure"] },
|
29
|
+
Port: 4713,
|
30
|
+
AccessLog: [],
|
31
|
+
Logger: WEBrick::Log.new(DEV_NULL))
|
32
|
+
end
|
33
|
+
Minitest.after_run { thread.exit }
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def teardown
|
38
|
+
Circuitbox.reset
|
39
|
+
end
|
40
|
+
|
41
|
+
def test_circuit_opens_cross_process
|
42
|
+
# Open the circuit via a different process
|
43
|
+
pid = fork do
|
44
|
+
con = Faraday.new do |c|
|
45
|
+
c.use FaradayMiddleware, identifier: "circuitbox_test_cross_process",
|
46
|
+
circuit_breaker_options: { cache: Moneta.new(:PStore, file: dbfile) }
|
47
|
+
end
|
48
|
+
open_circuit(con)
|
49
|
+
end
|
50
|
+
Process.wait pid
|
51
|
+
response = connection.get(failure_url)
|
52
|
+
assert response.original_response.nil?, "opening the circuit from a different process should be respected in the main process"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
data/test/integration_helper.rb
CHANGED
@@ -40,9 +40,9 @@ class FakeServer
|
|
40
40
|
end
|
41
41
|
|
42
42
|
module IntegrationHelpers
|
43
|
-
def open_circuit
|
44
|
-
volume_threshold = Circuitbox[
|
45
|
-
(volume_threshold + 1).times {
|
43
|
+
def open_circuit(c = connection)
|
44
|
+
volume_threshold = Circuitbox::CircuitBreaker::DEFAULTS[:volume_threshold]
|
45
|
+
(volume_threshold + 1).times { c.get(failure_url) }
|
46
46
|
end
|
47
47
|
end
|
48
48
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: circuitbox
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.11.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Fahim Ferdous
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2016-01-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -136,6 +136,20 @@ dependencies:
|
|
136
136
|
- - ">="
|
137
137
|
- !ruby/object:Gem::Version
|
138
138
|
version: '0'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: excon
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - ">="
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0'
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - ">="
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0'
|
139
153
|
- !ruby/object:Gem::Dependency
|
140
154
|
name: logger
|
141
155
|
requirement: !ruby/object:Gem::Requirement
|
@@ -164,6 +178,34 @@ dependencies:
|
|
164
178
|
- - ">="
|
165
179
|
- !ruby/object:Gem::Version
|
166
180
|
version: '0'
|
181
|
+
- !ruby/object:Gem::Dependency
|
182
|
+
name: lmdb
|
183
|
+
requirement: !ruby/object:Gem::Requirement
|
184
|
+
requirements:
|
185
|
+
- - ">="
|
186
|
+
- !ruby/object:Gem::Version
|
187
|
+
version: '0'
|
188
|
+
type: :development
|
189
|
+
prerelease: false
|
190
|
+
version_requirements: !ruby/object:Gem::Requirement
|
191
|
+
requirements:
|
192
|
+
- - ">="
|
193
|
+
- !ruby/object:Gem::Version
|
194
|
+
version: '0'
|
195
|
+
- !ruby/object:Gem::Dependency
|
196
|
+
name: daybreak
|
197
|
+
requirement: !ruby/object:Gem::Requirement
|
198
|
+
requirements:
|
199
|
+
- - ">="
|
200
|
+
- !ruby/object:Gem::Version
|
201
|
+
version: '0'
|
202
|
+
type: :development
|
203
|
+
prerelease: false
|
204
|
+
version_requirements: !ruby/object:Gem::Requirement
|
205
|
+
requirements:
|
206
|
+
- - ">="
|
207
|
+
- !ruby/object:Gem::Version
|
208
|
+
version: '0'
|
167
209
|
- !ruby/object:Gem::Dependency
|
168
210
|
name: activesupport
|
169
211
|
requirement: !ruby/object:Gem::Requirement
|
@@ -178,6 +220,20 @@ dependencies:
|
|
178
220
|
- - ">="
|
179
221
|
- !ruby/object:Gem::Version
|
180
222
|
version: '0'
|
223
|
+
- !ruby/object:Gem::Dependency
|
224
|
+
name: moneta
|
225
|
+
requirement: !ruby/object:Gem::Requirement
|
226
|
+
requirements:
|
227
|
+
- - ">="
|
228
|
+
- !ruby/object:Gem::Version
|
229
|
+
version: '0'
|
230
|
+
type: :runtime
|
231
|
+
prerelease: false
|
232
|
+
version_requirements: !ruby/object:Gem::Requirement
|
233
|
+
requirements:
|
234
|
+
- - ">="
|
235
|
+
- !ruby/object:Gem::Version
|
236
|
+
version: '0'
|
181
237
|
description: A robust circuit breaker that manages failing external services.
|
182
238
|
email:
|
183
239
|
- fahimfmf@gmail.com
|
@@ -186,17 +242,20 @@ extensions: []
|
|
186
242
|
extra_rdoc_files: []
|
187
243
|
files:
|
188
244
|
- ".gitignore"
|
245
|
+
- ".ruby-version"
|
189
246
|
- ".travis.yml"
|
190
247
|
- Gemfile
|
191
248
|
- LICENSE
|
192
249
|
- README.md
|
193
250
|
- Rakefile
|
251
|
+
- benchmark/circuit_store_benchmark.rb
|
194
252
|
- circuitbox.gemspec
|
195
253
|
- lib/circuitbox.rb
|
196
254
|
- lib/circuitbox/circuit_breaker.rb
|
197
255
|
- lib/circuitbox/errors/error.rb
|
198
256
|
- lib/circuitbox/errors/open_circuit_error.rb
|
199
257
|
- lib/circuitbox/errors/service_failure_error.rb
|
258
|
+
- lib/circuitbox/excon_middleware.rb
|
200
259
|
- lib/circuitbox/faraday_middleware.rb
|
201
260
|
- lib/circuitbox/memcache_store.rb
|
202
261
|
- lib/circuitbox/notifier.rb
|
@@ -205,7 +264,9 @@ files:
|
|
205
264
|
- lib/tasks/circuits.rake
|
206
265
|
- test/circuit_breaker_test.rb
|
207
266
|
- test/circuitbox_test.rb
|
267
|
+
- test/excon_middleware_test.rb
|
208
268
|
- test/faraday_middleware_test.rb
|
269
|
+
- test/integration/circuitbox_cross_process_open_test.rb
|
209
270
|
- test/integration/faraday_middleware_test.rb
|
210
271
|
- test/integration_helper.rb
|
211
272
|
- test/notifier_test.rb
|
@@ -238,7 +299,9 @@ summary: A robust circuit breaker that manages failing external services.
|
|
238
299
|
test_files:
|
239
300
|
- test/circuit_breaker_test.rb
|
240
301
|
- test/circuitbox_test.rb
|
302
|
+
- test/excon_middleware_test.rb
|
241
303
|
- test/faraday_middleware_test.rb
|
304
|
+
- test/integration/circuitbox_cross_process_open_test.rb
|
242
305
|
- test/integration/faraday_middleware_test.rb
|
243
306
|
- test/integration_helper.rb
|
244
307
|
- test/notifier_test.rb
|