circuitbox 0.10.4 → 0.11.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 +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
|