circuitbox 0.5.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/.gitignore +17 -0
- data/Gemfile +17 -0
- data/Guardfile +9 -0
- data/LICENSE +16 -0
- data/README.md +119 -0
- data/Rakefile +10 -0
- data/circuitbox.gemspec +25 -0
- data/lib/circuitbox/circuit_breaker.rb +235 -0
- data/lib/circuitbox/faraday_middleware.rb +31 -0
- data/lib/circuitbox/memcache_store.rb +31 -0
- data/lib/circuitbox/notifier.rb +12 -0
- data/lib/circuitbox/railtie.rb +10 -0
- data/lib/circuitbox/version.rb +3 -0
- data/lib/circuitbox.rb +70 -0
- data/lib/tasks/circuits.rake +12 -0
- data/test/circuit_breaker_test.rb +206 -0
- data/test/circuitbox_test.rb +73 -0
- data/test/faraday_middleware_test.rb +41 -0
- data/test/notifier_test.rb +10 -0
- data/test/test_helper.rb +6 -0
- metadata +111 -0
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
Y2JjODc0MzI1ZWFmMzk3NWExN2VjMTVmYTU5MDg1ZjBlMmI1YTc3OA==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
MTJiMjk1YWFkZThhY2FjZDdmYzA1MjUwYWY3OGNiNzE0N2JjMzM5OQ==
|
7
|
+
SHA512:
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
OTczMWYyMTZiZjQyZWQ0MmZjNTZmNjM4ZDU5MjI1NjA1YjQzYTZmMTY0OGY1
|
10
|
+
Y2EyYjIwNGZmYTEzMjY4MjVhYTIyODAwNDE5ZTE1Nzk2MWRiNWMyZDI3Yzgx
|
11
|
+
NzM4OWRhMWY1MWU0OTQzMWZiNmU4M2U3NzdiODgwOTE3MDllMmY=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
ODA5NDcxMjk0NDI1Mzc1ODU1OWQyMDQ5ZTA5ZWE4NWRhNDczNzFmOWFkMmY4
|
14
|
+
ZDBlNGYzYWEzNDgzMGY0YzM4YzZiZTNjMzI4ZjE2MGFmYzYyMTJiYmViNGEx
|
15
|
+
NDE5YWE1N2VmNjFkZWViNDI5MTdhZWM4ODFjMzRjNDNmNDM3Y2U=
|
data/.gitignore
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
# Specify your gem's dependencies in circuitbox.gemspec
|
4
|
+
gemspec
|
5
|
+
|
6
|
+
gem 'activesupport'
|
7
|
+
gem 'logger'
|
8
|
+
gem 'faraday'
|
9
|
+
|
10
|
+
group :test do
|
11
|
+
gem 'timecop'
|
12
|
+
gem 'mocha'
|
13
|
+
gem 'minitest'
|
14
|
+
gem 'guard-minitest'
|
15
|
+
gem 'gimme'
|
16
|
+
gem 'terminal-notifier-guard'
|
17
|
+
end
|
data/Guardfile
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
# A sample Guardfile
|
2
|
+
# More info at https://github.com/guard/guard#readme
|
3
|
+
|
4
|
+
guard :minitest do
|
5
|
+
# with Minitest::Unit
|
6
|
+
watch(%r{^test/(.*)_test\.rb$})
|
7
|
+
watch(%r{^lib/circuitbox/([^/]+)\.rb$}) { |m| "test/#{m[1]}_test.rb" }
|
8
|
+
watch(%r{^test/test_helper\.rb$}) { 'test' }
|
9
|
+
end
|
data/LICENSE
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
Copyright (c) 2014. Microsoft Corporation. All Rights Reserved
|
2
|
+
All rights reserved.
|
3
|
+
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
5
|
+
you may not use this file except in compliance with the License.
|
6
|
+
You may obtain a copy of the License at
|
7
|
+
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
9
|
+
|
10
|
+
THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
|
11
|
+
ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY
|
12
|
+
IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR
|
13
|
+
PURPOSE, MERCHANTABLITY OR NON-INFRINGEMENT.
|
14
|
+
|
15
|
+
See the Apache Version 2.0 License for specific language governing
|
16
|
+
permissions and limitations under the License.
|
data/README.md
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
# Circuitbox
|
2
|
+
|
3
|
+
Circuitbox is a Ruby circuit breaker gem. It protects your application from failures of it's service dependencies. It wraps calls to external services and monitors for failures in one minute intervals. Once more than 10 requests have been made with a 50% failure rate, Circuitbox stops sending requests to that failing service for one minute. This helps your application gracefully degrade and reduces the resources your application uses when calling failing services.
|
4
|
+
|
5
|
+
Resources about the circuit breaker pattern:
|
6
|
+
* [http://martinfowler.com/bliki/CircuitBreaker.html](http://martinfowler.com/bliki/CircuitBreaker.html)
|
7
|
+
* [https://github.com/Netflix/Hystrix/wiki/How-it-Works#CircuitBreaker](https://github.com/Netflix/Hystrix/wiki/How-it-Works#CircuitBreaker)
|
8
|
+
|
9
|
+
## Usage
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
Circuitbox[:your_service] do
|
13
|
+
Net::HTTP.get URI('http://example.com/api/messages')
|
14
|
+
end
|
15
|
+
```
|
16
|
+
|
17
|
+
Circuitbox will return nil for failed requests and open circuits.
|
18
|
+
If your HTTP client has it's own conditions for failure, you can pass an `exceptions` option.
|
19
|
+
|
20
|
+
```ruby
|
21
|
+
class ExampleServiceClient
|
22
|
+
def circuit
|
23
|
+
Circuitbox.circuit(:yammer, exceptions: [Zephyr::FailedRequest])
|
24
|
+
end
|
25
|
+
|
26
|
+
def http_get
|
27
|
+
circuit.run do
|
28
|
+
Zephyr.new("http://example.com").get(200, 1000, "/api/messages")
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
```
|
33
|
+
|
34
|
+
## Configuration
|
35
|
+
|
36
|
+
```ruby
|
37
|
+
class ExampleServiceClient
|
38
|
+
def circuit
|
39
|
+
Circuitbox.circuit(:your_service, {
|
40
|
+
exceptions: [YourCustomException],
|
41
|
+
|
42
|
+
# seconds the circuit stays open once it has passed the error threshold
|
43
|
+
sleep_window: 300,
|
44
|
+
|
45
|
+
# number of requests within 1 minute before it calculates error rates
|
46
|
+
volume_threshold: 10,
|
47
|
+
|
48
|
+
# exceeding this rate will open the circuit
|
49
|
+
error_threshold: 50,
|
50
|
+
|
51
|
+
# seconds before the circuit times out
|
52
|
+
timeout_seconds: 1
|
53
|
+
})
|
54
|
+
end
|
55
|
+
end
|
56
|
+
```
|
57
|
+
|
58
|
+
You can also pass a Proc as an option value which will evaluate each time the circuit breaker is used. This lets you configure the circuit breaker without having to restart the processes.
|
59
|
+
|
60
|
+
```ruby
|
61
|
+
Circuitbox.circuit(:yammer, {
|
62
|
+
sleep_window: Proc.new { Configuration.get(:sleep_window) }
|
63
|
+
})
|
64
|
+
```
|
65
|
+
|
66
|
+
## Monitoring & Statistics
|
67
|
+
|
68
|
+
You can also run `rake circuits:stats SERVICE={service_name}` to see successes, failures and opened circuits.
|
69
|
+
Add `PARTITION={partition_key}` to see the circuit for a particular partition.
|
70
|
+
The stats are aggregated into 1 minute intervals.
|
71
|
+
|
72
|
+
## Faraday (Caveat: Open circuits return a nil response object)
|
73
|
+
|
74
|
+
Circuitbox ships with [Faraday HTTP client](https://github.com/lostisland/faraday) middleware.
|
75
|
+
|
76
|
+
```ruby
|
77
|
+
require 'faraday'
|
78
|
+
require 'circuitbox/faraday_middleware'
|
79
|
+
|
80
|
+
conn = Faraday::Connection.new(:url => "http://example.com") do |builder|
|
81
|
+
builder.use Circuitbox::FaradayMiddleware
|
82
|
+
end
|
83
|
+
|
84
|
+
if response = conn.get("/api")
|
85
|
+
# success
|
86
|
+
else
|
87
|
+
# failure or open circuit
|
88
|
+
end
|
89
|
+
```
|
90
|
+
|
91
|
+
## TODO
|
92
|
+
* Fix Faraday integration to return a Faraday response object
|
93
|
+
* Split stats into it's own repository
|
94
|
+
* Circuit Breaker should raise an exception by default instead of returning nil
|
95
|
+
* Refactor to use single state variable
|
96
|
+
* Fix the partition hack
|
97
|
+
* Integrate with Breakerbox/Hystrix
|
98
|
+
|
99
|
+
## Installation
|
100
|
+
|
101
|
+
Add this line to your application's Gemfile:
|
102
|
+
|
103
|
+
gem 'circuitbox'
|
104
|
+
|
105
|
+
And then execute:
|
106
|
+
|
107
|
+
$ bundle
|
108
|
+
|
109
|
+
Or install it yourself as:
|
110
|
+
|
111
|
+
$ gem install circuitbox
|
112
|
+
|
113
|
+
## Contributing
|
114
|
+
|
115
|
+
1. Fork it
|
116
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
117
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
118
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
119
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
data/circuitbox.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
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
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "circuitbox"
|
8
|
+
spec.version = Circuitbox::VERSION
|
9
|
+
spec.authors = ["Fahim Ferdous"]
|
10
|
+
spec.email = ["fahimfmf@gmail.com"]
|
11
|
+
spec.description = %q{A robust circuit breaker that manages failing external services.}
|
12
|
+
spec.summary = %q{A robust circuit breaker that manages failing external services.}
|
13
|
+
spec.homepage = ""
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.4"
|
22
|
+
spec.add_development_dependency "rake"
|
23
|
+
|
24
|
+
spec.add_dependency "activesupport"
|
25
|
+
end
|
@@ -0,0 +1,235 @@
|
|
1
|
+
class Circuitbox
|
2
|
+
class CircuitBreaker
|
3
|
+
attr_accessor :service, :circuit_options, :exceptions, :partition,
|
4
|
+
:logger, :stat_store, :circuit_store, :notifier
|
5
|
+
|
6
|
+
DEFAULTS = {
|
7
|
+
sleep_window: 300,
|
8
|
+
volume_threshold: 5,
|
9
|
+
error_threshold: 50,
|
10
|
+
timeout_seconds: 1
|
11
|
+
}
|
12
|
+
|
13
|
+
#
|
14
|
+
# Configuration options
|
15
|
+
#
|
16
|
+
# `sleep_window` - seconds to sleep the circuit
|
17
|
+
# `volume_threshold` - number of requests before error rate calculation occurs
|
18
|
+
# `error_threshold` - percentage of failed requests needed to trip circuit
|
19
|
+
# `timeout_seconds` - seconds until it will timeout the request
|
20
|
+
# `exceptions` - exceptions other than Timeout::Error that count as failures
|
21
|
+
#
|
22
|
+
def initialize(service, options = {})
|
23
|
+
@service = service
|
24
|
+
@circuit_options = options
|
25
|
+
@circuit_store = options.fetch(:cache) { Circuitbox.circuit_store }
|
26
|
+
@notifier = Circuitbox::Notifier
|
27
|
+
|
28
|
+
@exceptions = options.fetch(:exceptions) { [] }
|
29
|
+
@exceptions = [Timeout::Error] if @exceptions.blank?
|
30
|
+
|
31
|
+
@logger = defined?(Rails) ? Rails.logger : Logger.new(STDOUT)
|
32
|
+
@stat_store = options.fetch(:stat_store) { Circuitbox.stat_store }
|
33
|
+
end
|
34
|
+
|
35
|
+
def option_value(name)
|
36
|
+
value = circuit_options.fetch(name) { DEFAULTS.fetch(name) }
|
37
|
+
value.is_a?(Proc) ? value.call : value
|
38
|
+
end
|
39
|
+
|
40
|
+
def run(run_options = {}, &block)
|
41
|
+
@partition = run_options.delete(:partition) # sorry for this hack.
|
42
|
+
cache_key = run_options.delete(:storage_key)
|
43
|
+
|
44
|
+
if open?
|
45
|
+
logger.debug "[CIRCUIT] open: skipping #{service}"
|
46
|
+
response = nil
|
47
|
+
open! unless open_flag?
|
48
|
+
else
|
49
|
+
logger.debug "[CIRCUIT] closed: querying #{service}"
|
50
|
+
|
51
|
+
begin
|
52
|
+
response = if exceptions.include? Timeout::Error
|
53
|
+
timeout_seconds = run_options.fetch(:timeout_seconds) { option_value(:timeout_seconds) }
|
54
|
+
timeout (timeout_seconds) { yield }
|
55
|
+
else
|
56
|
+
yield
|
57
|
+
end
|
58
|
+
|
59
|
+
logger.debug "[CIRCUIT] closed: #{service} querie success"
|
60
|
+
cache_response(cache_key, response) if cache_key
|
61
|
+
success!
|
62
|
+
rescue *exceptions => exception
|
63
|
+
logger.debug "[CIRCUIT] closed: detected #{service} failure"
|
64
|
+
failure!
|
65
|
+
response = cache_key ? get_cached_response(cache_key) : nil
|
66
|
+
open! if half_open?
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
return response
|
71
|
+
end
|
72
|
+
|
73
|
+
def open?
|
74
|
+
if open_flag?
|
75
|
+
true
|
76
|
+
elsif passed_volume_threshold? && passed_rate_threshold?
|
77
|
+
true
|
78
|
+
else
|
79
|
+
false
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def stats(partition)
|
84
|
+
@partition = partition
|
85
|
+
options = { without_partition: @partition.blank? }
|
86
|
+
|
87
|
+
stats = []
|
88
|
+
end_time = Time.now
|
89
|
+
hour = 48.hours.ago.change(min: 0, sec: 0)
|
90
|
+
while hour <= end_time
|
91
|
+
time_object = hour
|
92
|
+
|
93
|
+
60.times do |i|
|
94
|
+
time = time_object.change(min: i, sec: 0).to_i
|
95
|
+
stats << stats_for_time(time, options) unless time > Time.now.to_i
|
96
|
+
end
|
97
|
+
|
98
|
+
hour += 3600
|
99
|
+
end
|
100
|
+
stats
|
101
|
+
end
|
102
|
+
|
103
|
+
private
|
104
|
+
def open!
|
105
|
+
log_event :open
|
106
|
+
logger.debug "[CIRCUIT] opening #{service} circuit"
|
107
|
+
circuit_store.write(storage_key(:asleep), true, expires_in: option_value(:sleep_window).seconds)
|
108
|
+
half_open!
|
109
|
+
end
|
110
|
+
|
111
|
+
def half_open!
|
112
|
+
circuit_store.write(storage_key(:half_open), true)
|
113
|
+
end
|
114
|
+
|
115
|
+
def open_flag?
|
116
|
+
circuit_store.read(storage_key(:asleep)).present?
|
117
|
+
end
|
118
|
+
|
119
|
+
def half_open?
|
120
|
+
circuit_store.read(storage_key(:half_open)).present?
|
121
|
+
end
|
122
|
+
|
123
|
+
def passed_volume_threshold?
|
124
|
+
success_count + failure_count > option_value(:volume_threshold)
|
125
|
+
end
|
126
|
+
|
127
|
+
def passed_rate_threshold?
|
128
|
+
error_rate >= option_value(:error_threshold)
|
129
|
+
end
|
130
|
+
|
131
|
+
def failure_count
|
132
|
+
circuit_store.read(stat_storage_key(:failure)).to_i
|
133
|
+
end
|
134
|
+
|
135
|
+
def success_count
|
136
|
+
circuit_store.read(stat_storage_key(:success)).to_i
|
137
|
+
end
|
138
|
+
|
139
|
+
def error_rate
|
140
|
+
all_count = failure_count + success_count
|
141
|
+
return 0.0 unless all_count > 0
|
142
|
+
failure_count.to_f / all_count.to_f * 100
|
143
|
+
end
|
144
|
+
|
145
|
+
def success!
|
146
|
+
log_event :success
|
147
|
+
circuit_store.delete(storage_key(:half_open))
|
148
|
+
clear_failures!
|
149
|
+
end
|
150
|
+
|
151
|
+
def failure!
|
152
|
+
log_event :failure
|
153
|
+
end
|
154
|
+
|
155
|
+
# Store success/failure/open/close data in memcache
|
156
|
+
def log_event(event)
|
157
|
+
notifier.notify(event, service, partition)
|
158
|
+
log_event_to_process(event)
|
159
|
+
|
160
|
+
if stat_store.present?
|
161
|
+
log_event_to_stat_store(stat_storage_key(event))
|
162
|
+
log_event_to_stat_store(stat_storage_key(event, without_partition: true))
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
# When there is a successful response within a stat interval, clear the failures.
|
167
|
+
def clear_failures!
|
168
|
+
circuit_store.write(stat_storage_key(:failure), 0, raw: true)
|
169
|
+
end
|
170
|
+
|
171
|
+
# Logs to process memory.
|
172
|
+
def log_event_to_process(event)
|
173
|
+
key = stat_storage_key(event)
|
174
|
+
if circuit_store.read(key, raw: true)
|
175
|
+
circuit_store.increment(key)
|
176
|
+
else
|
177
|
+
circuit_store.write(key, 1, raw: true)
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
# Logs to Memcache.
|
182
|
+
def log_event_to_stat_store(key)
|
183
|
+
if stat_store.read(key, raw: true)
|
184
|
+
stat_store.increment(key)
|
185
|
+
else
|
186
|
+
stat_store.write(key, 1, raw: true)
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
# For returning stale responses when the circuit is open
|
191
|
+
def response_key(args)
|
192
|
+
Digest::SHA1.hexdigest(storage_key(:cache, args.inspect.to_s))
|
193
|
+
end
|
194
|
+
|
195
|
+
def cache_response(args, response)
|
196
|
+
cache.write(response_key(args), response)
|
197
|
+
end
|
198
|
+
|
199
|
+
def get_cached_response(args)
|
200
|
+
cache.read(response_key(args))
|
201
|
+
end
|
202
|
+
|
203
|
+
def stat_storage_key(event, options = {})
|
204
|
+
storage_key(:stats, Time.new.change(sec: 0).to_i, event, options)
|
205
|
+
end
|
206
|
+
|
207
|
+
def storage_key(*args)
|
208
|
+
options = args.extract_options!
|
209
|
+
|
210
|
+
key = if options[:without_partition]
|
211
|
+
"circuits:#{service}:#{args.join(":")}"
|
212
|
+
else
|
213
|
+
"circuits:#{service}:#{partition}:#{args.join(":")}"
|
214
|
+
end
|
215
|
+
|
216
|
+
return key
|
217
|
+
end
|
218
|
+
|
219
|
+
def timeout(timeout_seconds, &block)
|
220
|
+
Timeout::timeout(timeout_seconds) { block.call }
|
221
|
+
end
|
222
|
+
|
223
|
+
def self.reset
|
224
|
+
Circuitbox.reset
|
225
|
+
end
|
226
|
+
|
227
|
+
def stats_for_time(time, options = {})
|
228
|
+
stats = { time: time }
|
229
|
+
[:success, :failure, :open].each do |event|
|
230
|
+
stats[event] = stat_store.read(storage_key(:stats, time, event, options), raw: true) || 0
|
231
|
+
end
|
232
|
+
stats
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'faraday'
|
2
|
+
require 'circuitbox'
|
3
|
+
|
4
|
+
class Circuitbox
|
5
|
+
class RequestError < StandardError; end
|
6
|
+
|
7
|
+
class FaradayMiddleware < Faraday::Response::Middleware
|
8
|
+
|
9
|
+
attr_accessor :identifier, :exceptions
|
10
|
+
|
11
|
+
def initialize(app, opts={})
|
12
|
+
@identifier = opts.fetch(:identifier) { ->(env) { env.url }}
|
13
|
+
@exceptions = opts.fetch(:exceptions) { [Faraday::Error::TimeoutError] }
|
14
|
+
super(app)
|
15
|
+
end
|
16
|
+
|
17
|
+
def call(env)
|
18
|
+
id = identifier.respond_to?(:call) ? identifier.call(env) : identifier
|
19
|
+
circuit = Circuitbox.circuit id, :exceptions => exceptions
|
20
|
+
circuit.run do
|
21
|
+
super(env)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def on_complete(env)
|
26
|
+
if !env.success?
|
27
|
+
raise RequestError
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,31 @@
|
|
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
|
@@ -0,0 +1,12 @@
|
|
1
|
+
class Circuitbox
|
2
|
+
class Notifier
|
3
|
+
def self.notify(event, service, partition = nil)
|
4
|
+
return unless defined? ActiveSupport::Notifications
|
5
|
+
|
6
|
+
circuit_name = service
|
7
|
+
circuit_name += ":#{partition}" if partition
|
8
|
+
|
9
|
+
ActiveSupport::Notifications.instrument("circuit_#{event}", circuit: circuit_name)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
data/lib/circuitbox.rb
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
require 'active_support'
|
3
|
+
require 'logger'
|
4
|
+
require 'timeout'
|
5
|
+
|
6
|
+
require "circuitbox/version"
|
7
|
+
require 'circuitbox/memcache_store'
|
8
|
+
require 'circuitbox/railtie' if defined?(Rails)
|
9
|
+
require 'circuitbox/circuit_breaker'
|
10
|
+
require 'circuitbox/notifier'
|
11
|
+
|
12
|
+
class Circuitbox
|
13
|
+
attr_accessor :circuits, :circuit_store, :stat_store
|
14
|
+
cattr_accessor :configure
|
15
|
+
|
16
|
+
def self.instance
|
17
|
+
@@instance ||= new
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize
|
21
|
+
self.instance_eval(&@@configure) if @@configure
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.configure(&block)
|
25
|
+
@@configure = block if block
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.reset
|
29
|
+
@@instance = nil
|
30
|
+
@@configure = nil
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.circuit_store
|
34
|
+
self.instance.circuit_store ||= ActiveSupport::Cache::MemoryStore.new
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.circuit_store=(store)
|
38
|
+
self.instance.circuit_store = store
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.stat_store
|
42
|
+
self.instance.stat_store
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.stat_store=(store)
|
46
|
+
self.instance.stat_store = store
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.[](service_identifier, options = {})
|
50
|
+
self.circuit(service_identifier, options)
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.circuit(service_identifier, options = {})
|
54
|
+
service_name = self.parameter_to_service_name(service_identifier)
|
55
|
+
|
56
|
+
self.instance.circuits ||= Hash.new
|
57
|
+
self.instance.circuits[service_name] ||= CircuitBreaker.new(service_name, options)
|
58
|
+
|
59
|
+
if block_given?
|
60
|
+
self.instance.circuits[service_name].run { yield }
|
61
|
+
else
|
62
|
+
self.instance.circuits[service_name]
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.parameter_to_service_name(param)
|
67
|
+
uri = URI(param.to_s)
|
68
|
+
uri.host.present? ? uri.host : param.to_s
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
namespace :circuits do
|
2
|
+
task :stats => :environment do
|
3
|
+
service = ENV['SERVICE']
|
4
|
+
partition_key = ENV['PARTITION']
|
5
|
+
|
6
|
+
if service.blank?
|
7
|
+
raise "You must specify a SERVICE env variable, eg. `bundle exec rake circuits:stats SERVICE=yammer`"
|
8
|
+
else
|
9
|
+
pp Circuitbox::CircuitBreaker.new(service).stats(partition_key)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,206 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'circuitbox'
|
3
|
+
|
4
|
+
class CircuitBreakerTest < Minitest::Test
|
5
|
+
SUCCESSFUL_RESPONSE_STRING = "Success!"
|
6
|
+
RequestFailureError = Timeout::Error
|
7
|
+
class ConnectionError < StandardError; end;
|
8
|
+
class SomeOtherError < StandardError; end;
|
9
|
+
|
10
|
+
def setup
|
11
|
+
Circuitbox::CircuitBreaker.reset
|
12
|
+
end
|
13
|
+
|
14
|
+
def test_goes_into_half_open_state_on_sleep
|
15
|
+
circuit = Circuitbox::CircuitBreaker.new(:yammer)
|
16
|
+
circuit.send(:open!)
|
17
|
+
assert circuit.send(:half_open?)
|
18
|
+
end
|
19
|
+
|
20
|
+
describe "when in half open state" do
|
21
|
+
before do
|
22
|
+
Circuitbox::CircuitBreaker.reset
|
23
|
+
@circuit = Circuitbox::CircuitBreaker.new(:yammer)
|
24
|
+
end
|
25
|
+
|
26
|
+
it "opens circuit on next failed request" do
|
27
|
+
@circuit.stubs(half_open?: true)
|
28
|
+
@circuit.expects(:open!)
|
29
|
+
@circuit.run { raise RequestFailureError }
|
30
|
+
end
|
31
|
+
|
32
|
+
it "closes circuit on successful request" do
|
33
|
+
@circuit.send(:half_open!)
|
34
|
+
@circuit.run { 'success' }
|
35
|
+
assert !@circuit.send(:half_open?)
|
36
|
+
assert !@circuit.send(:open?)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def test_should_use_timeout_class_if_exceptions_are_not_defined
|
41
|
+
circuit = Circuitbox::CircuitBreaker.new(:yammer, timeout_seconds: 45)
|
42
|
+
circuit.expects(:timeout).with(45).once
|
43
|
+
emulate_circuit_run(circuit, :success, StandardError)
|
44
|
+
end
|
45
|
+
|
46
|
+
def test_should_not_use_timeout_class_if_custom_exceptions_are_defined
|
47
|
+
circuit = Circuitbox::CircuitBreaker.new(:yammer, exceptions: [ConnectionError])
|
48
|
+
circuit.expects(:timeout).never
|
49
|
+
emulate_circuit_run(circuit, :success, StandardError)
|
50
|
+
end
|
51
|
+
|
52
|
+
def test_should_return_response_if_it_doesnt_timeout
|
53
|
+
circuit = Circuitbox::CircuitBreaker.new(:yammer)
|
54
|
+
response = emulate_circuit_run(circuit, :success, SUCCESSFUL_RESPONSE_STRING)
|
55
|
+
assert_equal SUCCESSFUL_RESPONSE_STRING, response
|
56
|
+
end
|
57
|
+
|
58
|
+
def test_timeout_seconds_run_options_overrides_circuit_options
|
59
|
+
circuit = Circuitbox::CircuitBreaker.new(:yammer, timeout_seconds: 60)
|
60
|
+
circuit.expects(:timeout).with(30).once
|
61
|
+
circuit.run(timeout_seconds: 30) { true }
|
62
|
+
end
|
63
|
+
|
64
|
+
def test_catches_connection_error_failures_if_defined
|
65
|
+
circuit = Circuitbox::CircuitBreaker.new(:yammer, :exceptions => [ConnectionError])
|
66
|
+
response = emulate_circuit_run(circuit, :failure, ConnectionError)
|
67
|
+
assert_equal nil, response
|
68
|
+
end
|
69
|
+
|
70
|
+
def test_doesnt_catch_out_of_scope_exceptions
|
71
|
+
circuit = Circuitbox::CircuitBreaker.new(:yammer, :exceptions => [ConnectionError, RequestFailureError])
|
72
|
+
|
73
|
+
assert_raises SomeOtherError do
|
74
|
+
emulate_circuit_run(circuit, :failure, SomeOtherError)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def test_records_response_failure
|
79
|
+
circuit = Circuitbox::CircuitBreaker.new(:yammer, :exceptions => [RequestFailureError])
|
80
|
+
circuit.expects(:log_event).with(:failure)
|
81
|
+
emulate_circuit_run(circuit, :failure, RequestFailureError)
|
82
|
+
end
|
83
|
+
|
84
|
+
def test_records_response_success
|
85
|
+
circuit = Circuitbox::CircuitBreaker.new(:yammer)
|
86
|
+
circuit.expects(:log_event).with(:success)
|
87
|
+
emulate_circuit_run(circuit, :success, SUCCESSFUL_RESPONSE_STRING)
|
88
|
+
end
|
89
|
+
|
90
|
+
def test_does_not_send_request_if_circuit_is_open
|
91
|
+
circuit = Circuitbox::CircuitBreaker.new(:yammer)
|
92
|
+
circuit.stubs(:open? => true)
|
93
|
+
circuit.expects(:yield).never
|
94
|
+
response = emulate_circuit_run(circuit, :failure, RequestFailureError)
|
95
|
+
assert_equal nil, response
|
96
|
+
end
|
97
|
+
|
98
|
+
def test_returns_nil_response_on_failed_request
|
99
|
+
circuit = Circuitbox::CircuitBreaker.new(:yammer)
|
100
|
+
response = emulate_circuit_run(circuit, :failure, RequestFailureError)
|
101
|
+
assert_equal nil, response
|
102
|
+
end
|
103
|
+
|
104
|
+
def test_puts_circuit_to_sleep_once_opened
|
105
|
+
circuit = Circuitbox::CircuitBreaker.new(:yammer)
|
106
|
+
circuit.stubs(:open? => true)
|
107
|
+
|
108
|
+
assert !circuit.send(:open_flag?)
|
109
|
+
emulate_circuit_run(circuit, :failure, RequestFailureError)
|
110
|
+
assert circuit.send(:open_flag?)
|
111
|
+
|
112
|
+
circuit.expects(:open!).never
|
113
|
+
emulate_circuit_run(circuit, :failure, RequestFailureError)
|
114
|
+
end
|
115
|
+
|
116
|
+
def test_open_is_true_if_open_flag
|
117
|
+
circuit = Circuitbox::CircuitBreaker.new(:yammer)
|
118
|
+
circuit.stubs(:open_flag? => true)
|
119
|
+
assert circuit.open?
|
120
|
+
end
|
121
|
+
|
122
|
+
def test_open_checks_if_volume_threshold_has_passed
|
123
|
+
circuit = Circuitbox::CircuitBreaker.new(:yammer)
|
124
|
+
circuit.stubs(:open_flag? => false)
|
125
|
+
|
126
|
+
circuit.expects(:passed_volume_threshold?).once
|
127
|
+
circuit.open?
|
128
|
+
end
|
129
|
+
|
130
|
+
def test_open_checks_error_rate_threshold
|
131
|
+
circuit = Circuitbox::CircuitBreaker.new(:yammer)
|
132
|
+
circuit.stubs(:open_flag? => false,
|
133
|
+
:passed_volume_threshold? => true)
|
134
|
+
|
135
|
+
circuit.expects(:passed_rate_threshold?).once
|
136
|
+
circuit.open?
|
137
|
+
end
|
138
|
+
|
139
|
+
def test_open_is_false_if_awake_and_under_rate_threshold
|
140
|
+
circuit = Circuitbox::CircuitBreaker.new(:yammer)
|
141
|
+
circuit.stubs(:open_flag? => false,
|
142
|
+
:passed_volume_threshold? => false,
|
143
|
+
:passed_rate_threshold => false)
|
144
|
+
|
145
|
+
assert !circuit.open?
|
146
|
+
end
|
147
|
+
|
148
|
+
def test_error_rate_threshold_calculation
|
149
|
+
circuit = Circuitbox::CircuitBreaker.new(:yammer)
|
150
|
+
circuit.stubs(:failure_count => 3, :success_count => 2)
|
151
|
+
assert circuit.send(:passed_rate_threshold?)
|
152
|
+
|
153
|
+
circuit.stubs(:failure_count => 2, :success_count => 3)
|
154
|
+
assert !circuit.send(:passed_rate_threshold?)
|
155
|
+
end
|
156
|
+
|
157
|
+
def test_logs_and_retrieves_success_events
|
158
|
+
circuit = Circuitbox::CircuitBreaker.new(:yammer)
|
159
|
+
5.times { circuit.send(:log_event, :success) }
|
160
|
+
assert_equal 5, circuit.send(:success_count)
|
161
|
+
end
|
162
|
+
|
163
|
+
def test_logs_and_retrieves_failure_events
|
164
|
+
circuit = Circuitbox::CircuitBreaker.new(:yammer)
|
165
|
+
5.times { circuit.send(:log_event, :failure) }
|
166
|
+
assert_equal 5, circuit.send(:failure_count)
|
167
|
+
end
|
168
|
+
|
169
|
+
def test_logs_events_by_minute
|
170
|
+
circuit = Circuitbox::CircuitBreaker.new(:yammer)
|
171
|
+
|
172
|
+
Timecop.travel(Time.now.change(sec: 5))
|
173
|
+
4.times { circuit.send(:log_event, :success) }
|
174
|
+
assert_equal 4, circuit.send(:success_count)
|
175
|
+
|
176
|
+
Timecop.travel(1.minute.from_now)
|
177
|
+
7.times { circuit.send(:log_event, :success) }
|
178
|
+
assert_equal 7, circuit.send(:success_count)
|
179
|
+
|
180
|
+
Timecop.travel(30.seconds.from_now)
|
181
|
+
circuit.send(:log_event, :success)
|
182
|
+
assert_equal 8, circuit.send(:success_count)
|
183
|
+
|
184
|
+
Timecop.travel(50.seconds.from_now)
|
185
|
+
assert_equal 0, circuit.send(:success_count)
|
186
|
+
end
|
187
|
+
|
188
|
+
def test_notifies_on_open_circuit
|
189
|
+
circuit = Circuitbox::CircuitBreaker.new(:yammer)
|
190
|
+
Circuitbox::Notifier.expects(:notify).with(:open, :yammer, nil)
|
191
|
+
circuit.send(:log_event, :open)
|
192
|
+
end
|
193
|
+
|
194
|
+
def emulate_circuit_run(circuit, response_type, response_value)
|
195
|
+
circuit.run do
|
196
|
+
case response_type
|
197
|
+
when :failure
|
198
|
+
raise response_value
|
199
|
+
when :success
|
200
|
+
response_value
|
201
|
+
end
|
202
|
+
end
|
203
|
+
rescue RequestFailureError
|
204
|
+
nil
|
205
|
+
end
|
206
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'circuitbox'
|
3
|
+
|
4
|
+
class Circuitbox::ExampleStore < ActiveSupport::Cache::MemoryStore; end
|
5
|
+
|
6
|
+
describe Circuitbox do
|
7
|
+
before { Circuitbox.reset }
|
8
|
+
after { Circuitbox.reset }
|
9
|
+
|
10
|
+
describe "Circuitbox.configure" do
|
11
|
+
it "configures instance variables on init" do
|
12
|
+
Circuitbox.configure do
|
13
|
+
self.stat_store = "hello"
|
14
|
+
end
|
15
|
+
|
16
|
+
assert_equal "hello", Circuitbox.stat_store
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
describe "Circuitbox.circuit_store" do
|
21
|
+
it "is configurable" do
|
22
|
+
example_store = Circuitbox::ExampleStore.new
|
23
|
+
Circuitbox.circuit_store = example_store
|
24
|
+
assert_equal example_store, Circuitbox[:yammer].circuit_store
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe "Circuitbox.stat_store" do
|
29
|
+
it "is configurable" do
|
30
|
+
example_store = Circuitbox::ExampleStore.new
|
31
|
+
Circuitbox.stat_store = example_store
|
32
|
+
assert_equal example_store, Circuitbox[:yammer].stat_store
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe "Circuitbox[:service]" do
|
37
|
+
it "delegates to #circuit" do
|
38
|
+
Circuitbox.expects(:circuit).with(:yammer, {})
|
39
|
+
Circuitbox[:yammer]
|
40
|
+
end
|
41
|
+
|
42
|
+
it "creates a CircuitBreaker instance" do
|
43
|
+
assert Circuitbox[:yammer].is_a? Circuitbox::CircuitBreaker
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe "#circuit" do
|
48
|
+
it "returns the same circuit every time" do
|
49
|
+
assert_equal Circuitbox.circuit(:yammer).object_id, Circuitbox.circuit(:yammer).object_id
|
50
|
+
end
|
51
|
+
|
52
|
+
it "sets the circuit options the first time" do
|
53
|
+
circuit_one = Circuitbox.circuit(:yammer, :sleep_window => 1337)
|
54
|
+
circuit_two = Circuitbox.circuit(:yammer, :sleep_window => 2000)
|
55
|
+
|
56
|
+
assert_equal 1337, circuit_one.option_value(:sleep_window)
|
57
|
+
assert_equal 1337, circuit_two.option_value(:sleep_window)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
describe "#parameter_to_service_name" do
|
62
|
+
it "parses out a service name from URI" do
|
63
|
+
service = Circuitbox.parameter_to_service_name("http://api.yammer.com/api/v1/messages")
|
64
|
+
assert_equal "api.yammer.com", service
|
65
|
+
end
|
66
|
+
|
67
|
+
it "uses the parameter as the service name if the parameter is not an URI" do
|
68
|
+
service = Circuitbox.parameter_to_service_name(:yammer)
|
69
|
+
assert_equal "yammer", service
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require_relative '../lib/circuitbox/faraday_middleware'
|
3
|
+
|
4
|
+
class Circuitbox
|
5
|
+
class FaradayMiddlewareTest < Minitest::Test
|
6
|
+
|
7
|
+
def setup
|
8
|
+
@app = gimme
|
9
|
+
@env = gimme
|
10
|
+
give(@env).url { 'URL' }
|
11
|
+
|
12
|
+
@middleware = FaradayMiddleware.new @app,
|
13
|
+
:identifier => 'ID',
|
14
|
+
:exceptions => [StandardError]
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_should_use_env_url_proc_if_not_provided_as_identifier
|
18
|
+
middleware = FaradayMiddleware.new @app, :exceptions => gimme
|
19
|
+
assert middleware.identifier.is_a?(Proc)
|
20
|
+
assert_equal 'URL', middleware.identifier.call(@env)
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_should_use_request_error_if_not_provided_as_exception
|
24
|
+
middleware = FaradayMiddleware.new @app, :identifier => 'ID'
|
25
|
+
assert_equal [Faraday::Error::TimeoutError],
|
26
|
+
middleware.exceptions
|
27
|
+
end
|
28
|
+
|
29
|
+
def test_successful_call
|
30
|
+
@middleware.call(@env)
|
31
|
+
end
|
32
|
+
|
33
|
+
def test_failed_call
|
34
|
+
assert_raises Circuitbox::RequestError do
|
35
|
+
give(@env).success? { false }
|
36
|
+
@middleware.on_complete(@env)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'circuitbox/notifier'
|
3
|
+
require 'active_support/notifications'
|
4
|
+
|
5
|
+
describe Circuitbox::Notifier do
|
6
|
+
it "sends an ActiveSupport::Notification" do
|
7
|
+
ActiveSupport::Notifications.expects(:instrument).with("circuit_open", circuit: :yammer)
|
8
|
+
Circuitbox::Notifier.notify(:open, :yammer)
|
9
|
+
end
|
10
|
+
end
|
data/test/test_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,111 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: circuitbox
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.5.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Fahim Ferdous
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-08-13 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ~>
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.4'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.4'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ! '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ! '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: activesupport
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ! '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
description: A robust circuit breaker that manages failing external services.
|
56
|
+
email:
|
57
|
+
- fahimfmf@gmail.com
|
58
|
+
executables: []
|
59
|
+
extensions: []
|
60
|
+
extra_rdoc_files: []
|
61
|
+
files:
|
62
|
+
- .gitignore
|
63
|
+
- Gemfile
|
64
|
+
- Guardfile
|
65
|
+
- LICENSE
|
66
|
+
- README.md
|
67
|
+
- Rakefile
|
68
|
+
- circuitbox.gemspec
|
69
|
+
- lib/circuitbox.rb
|
70
|
+
- lib/circuitbox/circuit_breaker.rb
|
71
|
+
- lib/circuitbox/faraday_middleware.rb
|
72
|
+
- lib/circuitbox/memcache_store.rb
|
73
|
+
- lib/circuitbox/notifier.rb
|
74
|
+
- lib/circuitbox/railtie.rb
|
75
|
+
- lib/circuitbox/version.rb
|
76
|
+
- lib/tasks/circuits.rake
|
77
|
+
- test/circuit_breaker_test.rb
|
78
|
+
- test/circuitbox_test.rb
|
79
|
+
- test/faraday_middleware_test.rb
|
80
|
+
- test/notifier_test.rb
|
81
|
+
- test/test_helper.rb
|
82
|
+
homepage: ''
|
83
|
+
licenses:
|
84
|
+
- MIT
|
85
|
+
metadata: {}
|
86
|
+
post_install_message:
|
87
|
+
rdoc_options: []
|
88
|
+
require_paths:
|
89
|
+
- lib
|
90
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
91
|
+
requirements:
|
92
|
+
- - ! '>='
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
version: '0'
|
95
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
96
|
+
requirements:
|
97
|
+
- - ! '>='
|
98
|
+
- !ruby/object:Gem::Version
|
99
|
+
version: '0'
|
100
|
+
requirements: []
|
101
|
+
rubyforge_project:
|
102
|
+
rubygems_version: 2.2.2
|
103
|
+
signing_key:
|
104
|
+
specification_version: 4
|
105
|
+
summary: A robust circuit breaker that manages failing external services.
|
106
|
+
test_files:
|
107
|
+
- test/circuit_breaker_test.rb
|
108
|
+
- test/circuitbox_test.rb
|
109
|
+
- test/faraday_middleware_test.rb
|
110
|
+
- test/notifier_test.rb
|
111
|
+
- test/test_helper.rb
|