all_my_circuits 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/Gemfile +4 -0
- data/LICENSE +23 -0
- data/README.md +77 -0
- data/Rakefile +10 -0
- data/all_my_circuits.gemspec +28 -0
- data/all_my_circuits.jpg +0 -0
- data/circle.yml +12 -0
- data/lib/all_my_circuits/breaker.rb +264 -0
- data/lib/all_my_circuits/clock.rb +9 -0
- data/lib/all_my_circuits/exceptions.rb +6 -0
- data/lib/all_my_circuits/logging.rb +23 -0
- data/lib/all_my_circuits/notifiers/abstract_notifier.rb +27 -0
- data/lib/all_my_circuits/notifiers/null_notifier.rb +18 -0
- data/lib/all_my_circuits/notifiers.rb +8 -0
- data/lib/all_my_circuits/null_breaker.rb +23 -0
- data/lib/all_my_circuits/strategies/abstract_strategy.rb +40 -0
- data/lib/all_my_circuits/strategies/abstract_window_strategy/window.rb +45 -0
- data/lib/all_my_circuits/strategies/abstract_window_strategy.rb +33 -0
- data/lib/all_my_circuits/strategies/number_over_window_strategy.rb +32 -0
- data/lib/all_my_circuits/strategies/percentage_over_window_strategy.rb +32 -0
- data/lib/all_my_circuits/strategies.rb +10 -0
- data/lib/all_my_circuits/version.rb +3 -0
- data/lib/all_my_circuits.rb +20 -0
- data/script/fake_service.rb +46 -0
- data/script/graphing_server.rb +126 -0
- data/script/graphing_stress_test.rb +80 -0
- metadata +142 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 37ff5fd01761190c1ac3b39e9716cac0b242e244
|
4
|
+
data.tar.gz: 3978e9a09c6bc75e86227359036f783eb33616b6
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ea62b8b682a65ecd8b1e03f02820040db8023b0de594ae0a1153988674514a665c504284d568fce3950fab060917892e55edc1ec217fe42178b549995f63eb38
|
7
|
+
data.tar.gz: a79c6a02bcad7e76310b75899b22b0b926f1d9a6e8b750d506d29eed1eb19354de9dbccbfbb0bacf2353d7f71053b12453ac7c77daadb1b725b930942e463a66
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
Copyright (c) 2015, Remind101, Inc.
|
2
|
+
All rights reserved.
|
3
|
+
|
4
|
+
Redistribution and use in source and binary forms, with or without
|
5
|
+
modification, are permitted provided that the following conditions are met:
|
6
|
+
|
7
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
8
|
+
list of conditions and the following disclaimer.
|
9
|
+
|
10
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
11
|
+
this list of conditions and the following disclaimer in the documentation
|
12
|
+
and/or other materials provided with the distribution.
|
13
|
+
|
14
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
15
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
16
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
17
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
18
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
19
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
20
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
21
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
22
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
23
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
data/README.md
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
# AllMyCircuits
|
2
|
+
|
3
|
+
[](https://circleci.com/gh/remind101/all_my_circuits)
|
4
|
+
|
5
|
+

|
6
|
+
|
7
|
+
AllMyCircuits is intended to be a threadsafe-ish circuit breaker implementation for Ruby.
|
8
|
+
See [Martin Fowler's article on the Circuit Breaker pattern for more info](http://martinfowler.com/bliki/CircuitBreaker.html).
|
9
|
+
|
10
|
+
# Usage
|
11
|
+
|
12
|
+
class MyService
|
13
|
+
include Singleton
|
14
|
+
|
15
|
+
def initialize
|
16
|
+
@circuit_breaker = AllMyCircuits::Breaker.new(
|
17
|
+
name: "my_service",
|
18
|
+
watch_errors: [Timeout::Error], # defaults to AllMyCircuits::Breaker.net_errors,
|
19
|
+
# which includes typical net/http and socket errors
|
20
|
+
sleep_seconds: 10, # leave circuit open for 10 seconds, than try to call the service again
|
21
|
+
strategy: AllMyCircuits::Strategies::PercentageOverWindowStrategy.new(
|
22
|
+
requests_window: 100, # number of requests to calculate the average failure rate for
|
23
|
+
failure_rate_percent_threshold: 25 # open circuit if 25% or more requests within 100-request window fail
|
24
|
+
# must trip open again if the first request fails
|
25
|
+
}
|
26
|
+
)
|
27
|
+
end
|
28
|
+
|
29
|
+
def run
|
30
|
+
begin
|
31
|
+
@breaker.run do
|
32
|
+
Timeout.timeout(1.0) { my_risky_call }
|
33
|
+
end
|
34
|
+
rescue AllMyCircuits::BreakerOpen => e
|
35
|
+
# log me somewhere
|
36
|
+
rescue
|
37
|
+
# uh-oh, risky call failed once
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Testing
|
43
|
+
|
44
|
+
So, what have we got:
|
45
|
+
|
46
|
+
* Time-sensitive code: ...check
|
47
|
+
* Concurrent code: ...check
|
48
|
+
|
49
|
+
Dude, that's a real headache for someone who's not confortable enough with concurrent code.
|
50
|
+
I haven't figured any awesome way of automated testing in this case.
|
51
|
+
So, in the [script](https://github.com/remind101/all_my_circuits/tree/master/script) folder, there are:
|
52
|
+
|
53
|
+
* [fake_service.rb](https://github.com/remind101/all_my_circuits/blob/master/script/fake_service.rb)
|
54
|
+
* [graphing_stress_test.rb](https://github.com/remind101/all_my_circuits/blob/master/script/graphing_stress_test.rb)
|
55
|
+
|
56
|
+
## fake_service.rb
|
57
|
+
|
58
|
+
the Fake Service has 3 modes of operation: `up` (default), `die` and `slowdown`.
|
59
|
+
|
60
|
+
* `up` (default) - http://localhost:8081/up - normal mode of operation, latency up to 50ms
|
61
|
+
* `die` - http://localhost:8081/die - exceptions are raised left and right, slight delay in response
|
62
|
+
* `slowdown` - http://localhost:8081/slowdown - successful responses with a significant delay.
|
63
|
+
|
64
|
+
## Graphing Stress Test
|
65
|
+
|
66
|
+
runs `WORKERS` number of workers which continuously hit http://localhost:8081. Graphs are served at http://localhost:8080.
|
67
|
+
This app allows to catch incorrect circuit breaker behavior visually.
|
68
|
+
|
69
|
+
# Logging
|
70
|
+
|
71
|
+
Logger can be configured by setting `AllMyCircuits.logger`.
|
72
|
+
Default log device is STDERR, and default level is ERROR (can be overridden with `ALL_MY_CIRCUITS_LOG_LEVEL` environment variable).
|
73
|
+
|
74
|
+
# TODO
|
75
|
+
|
76
|
+
* global controls through redis?
|
77
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'all_my_circuits/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "all_my_circuits"
|
8
|
+
spec.version = AllMyCircuits::VERSION
|
9
|
+
spec.authors = ["Vlad Yarotsky"]
|
10
|
+
spec.email = ["vlad@remind101.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{Circuit Breaker library with support for rolling-window absolute/percentage thresholds}
|
13
|
+
spec.description = %q{}
|
14
|
+
spec.homepage = "https://github.com/remind101/all_my_circuits"
|
15
|
+
spec.license = "BSD"
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
18
|
+
spec.bindir = "exe"
|
19
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
20
|
+
spec.require_paths = ["lib"]
|
21
|
+
|
22
|
+
spec.add_dependency "concurrent-ruby", "~> 0.8"
|
23
|
+
|
24
|
+
spec.add_development_dependency "bundler", "~> 1.9"
|
25
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
26
|
+
spec.add_development_dependency "minitest", "~> 5.6"
|
27
|
+
spec.add_development_dependency "puma", "~> 2.11"
|
28
|
+
end
|
data/all_my_circuits.jpg
ADDED
Binary file
|
data/circle.yml
ADDED
@@ -0,0 +1,264 @@
|
|
1
|
+
require "concurrent/atomic"
|
2
|
+
require "thread"
|
3
|
+
|
4
|
+
module AllMyCircuits
|
5
|
+
|
6
|
+
class Breaker
|
7
|
+
include Logging
|
8
|
+
|
9
|
+
attr_reader :name
|
10
|
+
|
11
|
+
# Public: exceptions typically thrown when using Net::HTTP
|
12
|
+
#
|
13
|
+
def self.net_errors
|
14
|
+
require "timeout"
|
15
|
+
require "net/http"
|
16
|
+
|
17
|
+
[Timeout::Error, Errno::ECONNRESET, Errno::ECONNREFUSED, EOFError,
|
18
|
+
Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::HTTPFatalError, Net::HTTPServerError]
|
19
|
+
end
|
20
|
+
|
21
|
+
# Public: Initializes circuit breaker instance.
|
22
|
+
#
|
23
|
+
# Options
|
24
|
+
#
|
25
|
+
# name - name of the call wrapped into circuit breaker (e.g. "That Unstable Service").
|
26
|
+
# watch_errors - exceptions to count as failures. Other exceptions will simply get re-raised
|
27
|
+
# (default: AllMyCircuits::Breaker.net_errors).
|
28
|
+
# sleep_seconds - number of seconds the circuit stays open before attempting to close.
|
29
|
+
# strategy - an AllMyCircuits::Strategies::AbstractStrategy-compliant object that controls
|
30
|
+
# when the circuit should be tripped open.
|
31
|
+
# Built-in strategies:
|
32
|
+
# AllMyCircuits::Strategies::PercentageOverWindowStrategy,
|
33
|
+
# AllMyCircuits::Strategies::NumberOverWindowStrategy.
|
34
|
+
# notifier - (optional) AllMyCircuits::Notifiers::AbstractNotifier-compliant object that
|
35
|
+
# is called whenever circuit breaker state (open, closed) changes.
|
36
|
+
# Built-in notifiers:
|
37
|
+
# AllMyCircuits::Notifiers::NullNotifier.
|
38
|
+
#
|
39
|
+
# Examples
|
40
|
+
#
|
41
|
+
# AllMyCircuits::Breaker.new(
|
42
|
+
# name: "My Unstable Service",
|
43
|
+
# sleep_seconds: 5,
|
44
|
+
# strategy: AllMyCircuits::Strategies::PercentageOverWindowStrategy.new(
|
45
|
+
# requests_window: 20, # number of requests in the window to calculate failure rate for
|
46
|
+
# failure_rate_percent_threshold: 25 # how many failures can occur within the window, in percent,
|
47
|
+
# ) # before the circuit opens
|
48
|
+
# )
|
49
|
+
#
|
50
|
+
# AllMyCircuits::Breaker.new(
|
51
|
+
# name: "Another Unstable Service",
|
52
|
+
# sleep_seconds: 5,
|
53
|
+
# strategy: AllMyCircuits::Strategies::NumberOverWindowStrategy.new(
|
54
|
+
# requests_window: 20,
|
55
|
+
# failures_threshold: 25 # how many failures can occur within the window before the circuit opens
|
56
|
+
# )
|
57
|
+
# )
|
58
|
+
#
|
59
|
+
def initialize(name:,
|
60
|
+
watch_errors: Breaker.net_errors,
|
61
|
+
sleep_seconds:,
|
62
|
+
strategy:,
|
63
|
+
notifier: Notifiers::NullNotifier.new,
|
64
|
+
clock: Clock)
|
65
|
+
|
66
|
+
@name = String(name).dup.freeze
|
67
|
+
@watch_errors = Array(watch_errors).dup
|
68
|
+
@sleep_seconds = Integer(sleep_seconds)
|
69
|
+
|
70
|
+
@strategy = strategy
|
71
|
+
@notifier = notifier
|
72
|
+
|
73
|
+
@state_lock = Mutex.new
|
74
|
+
@request_number = Concurrent::Atomic.new(0)
|
75
|
+
@last_open_or_probed = nil
|
76
|
+
@opened_at_request_number = 0
|
77
|
+
@clock = clock
|
78
|
+
end
|
79
|
+
|
80
|
+
# Public: executes supplied block of code and monitors failures.
|
81
|
+
# Once the number of failures reaches a certain threshold, the block is bypassed
|
82
|
+
# for a certain period of time.
|
83
|
+
#
|
84
|
+
# Consider the following examples of calls through circuit breaker (let it be 1 call per second,
|
85
|
+
# and let the circuit breaker be configured as in the example below):
|
86
|
+
#
|
87
|
+
# Legend
|
88
|
+
#
|
89
|
+
# S - successful request
|
90
|
+
# F - failed request
|
91
|
+
# O - skipped request (circuit open)
|
92
|
+
# | - open circuit interval end
|
93
|
+
#
|
94
|
+
#
|
95
|
+
# 1) S S F F S S F F S F O O O O O|S S S S S
|
96
|
+
#
|
97
|
+
# Here among the first 10 requests (window), 5 failures occur (50%), the circuit is tripped open
|
98
|
+
# for 5 seconds, and a few requests are skipped. Then, after 5 seconds, a request is issued to
|
99
|
+
# see whether everything is back to normal, and the circuit is then closed again.
|
100
|
+
#
|
101
|
+
# 2) S S F F S S F F S F O O O O O|F O O O O O|S S S S S
|
102
|
+
#
|
103
|
+
# Same situation, 10 requests, 5 failed, circuit is tripped open for 5 seconds. Then we
|
104
|
+
# check that service is back to normal, and it is not. The circuit is open again.
|
105
|
+
# After another 5 seconds we check again and close the circuit.
|
106
|
+
#
|
107
|
+
# Returns nothing.
|
108
|
+
# Raises AllMyCircuit::BreakerOpen with the name of the service when the circuit is open.
|
109
|
+
# Raises whatever has been raised by the supplied block.
|
110
|
+
#
|
111
|
+
# This call is thread-safe sans the supplied block.
|
112
|
+
#
|
113
|
+
# Examples
|
114
|
+
#
|
115
|
+
# @cb = AllMyCircuits::Breaker.new(
|
116
|
+
# name: "that bad service",
|
117
|
+
# sleep_seconds: 5,
|
118
|
+
# strategy: AllMyCircuits::Strategies::PercentageOverWindowStrategy.new(
|
119
|
+
# requests_window: 10,
|
120
|
+
# failure_rate_percent_threshold: 50
|
121
|
+
# )
|
122
|
+
# )
|
123
|
+
#
|
124
|
+
# @client = MyBadServiceClient.new(timeout: 2)
|
125
|
+
#
|
126
|
+
# begin
|
127
|
+
# @cb.run do
|
128
|
+
# @client.make_expensive_unreliable_http_call
|
129
|
+
# end
|
130
|
+
# rescue AllMyCircuits::BreakerOpen => e
|
131
|
+
# []
|
132
|
+
# rescue MyBadServiceClient::Error => e
|
133
|
+
# MyLog << "an error has occured in call to my bad service"
|
134
|
+
# []
|
135
|
+
# end
|
136
|
+
#
|
137
|
+
def run
|
138
|
+
unless allow_request?
|
139
|
+
debug "declining request, circuit is open", name
|
140
|
+
raise BreakerOpen, @name
|
141
|
+
end
|
142
|
+
|
143
|
+
current_request_number = generate_request_number
|
144
|
+
begin
|
145
|
+
result = yield
|
146
|
+
success(current_request_number)
|
147
|
+
result
|
148
|
+
rescue *@watch_errors
|
149
|
+
error(current_request_number)
|
150
|
+
raise
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
private
|
155
|
+
|
156
|
+
# Internal: checks whether the circuit is closed, or if it is time
|
157
|
+
# to try one request to see if things are back to normal.
|
158
|
+
#
|
159
|
+
def allow_request?
|
160
|
+
@state_lock.synchronize do
|
161
|
+
!open? || allow_probe_request?
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
def generate_request_number
|
166
|
+
current_request_number = @request_number.update { |v| v + 1 }
|
167
|
+
|
168
|
+
debug "new request", name, current_request_number
|
169
|
+
current_request_number
|
170
|
+
end
|
171
|
+
|
172
|
+
# Internal: markes request as successful. Closes the circuit if necessary.
|
173
|
+
#
|
174
|
+
# Arguments
|
175
|
+
# current_request_number - the number assigned to the request by the circuit breaker
|
176
|
+
# before it was sent.
|
177
|
+
#
|
178
|
+
def success(current_request_number)
|
179
|
+
will_notify_notifier_closed = false
|
180
|
+
|
181
|
+
@state_lock.synchronize do
|
182
|
+
if open?
|
183
|
+
# This ensures that we are not closing the circuit prematurely
|
184
|
+
# due to a response for an old request coming in.
|
185
|
+
if current_request_number > @opened_at_request_number
|
186
|
+
info "closing circuit", name, current_request_number
|
187
|
+
close!
|
188
|
+
will_notify_notifier_closed = true
|
189
|
+
else
|
190
|
+
debug "ignoring late success response", name, current_request_number
|
191
|
+
end
|
192
|
+
end
|
193
|
+
debug "request succeeded", name, current_request_number
|
194
|
+
@strategy.success
|
195
|
+
end
|
196
|
+
|
197
|
+
# We don't want to be doing this while holding the lock
|
198
|
+
if will_notify_notifier_closed
|
199
|
+
@notifier.closed
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
# Internal: marks request as failed. Opens the circuit if necessary.
|
204
|
+
#
|
205
|
+
def error(current_request_number)
|
206
|
+
will_notify_notifier_opened = false
|
207
|
+
|
208
|
+
@state_lock.synchronize do
|
209
|
+
if open?
|
210
|
+
debug "ignoring late error response (circuit is open)", name, current_request_number
|
211
|
+
return
|
212
|
+
end
|
213
|
+
|
214
|
+
debug "request failed. #{@strategy.inspect}", name, current_request_number
|
215
|
+
@strategy.error
|
216
|
+
|
217
|
+
if @strategy.should_open?
|
218
|
+
info "opening circuit", name, current_request_number
|
219
|
+
open!
|
220
|
+
will_notify_notifier_opened = true
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
# We don't want to be doing this while holding the lock
|
225
|
+
if will_notify_notifier_opened
|
226
|
+
@notifier.opened
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
def open?
|
231
|
+
@opened_at_request_number > 0
|
232
|
+
end
|
233
|
+
|
234
|
+
def allow_probe_request?
|
235
|
+
if open? && @clock.timestamp >= (@last_open_or_probed + @sleep_seconds)
|
236
|
+
debug "allowing probe request", name
|
237
|
+
# makes sure that we allow only one probe request by extending sleep interval
|
238
|
+
# and leaving the circuit open until closed by the success callback.
|
239
|
+
@last_open_or_probed = @clock.timestamp
|
240
|
+
return true
|
241
|
+
end
|
242
|
+
false
|
243
|
+
end
|
244
|
+
|
245
|
+
def open!
|
246
|
+
@last_open_or_probed = @clock.timestamp
|
247
|
+
# The most recent request encountered so far (may not be the current request in concurrent situation).
|
248
|
+
# This is necessary to prevent successful response to old request from opening the circuit prematurely.
|
249
|
+
# Imagine concurrent situation ("|" for request start, ">" for request end):
|
250
|
+
# 1|-----------> success
|
251
|
+
# 2|----> error, open circuit breaker
|
252
|
+
# In this case request 1) should not close the circuit.
|
253
|
+
@opened_at_request_number = @request_number.value
|
254
|
+
@strategy.opened
|
255
|
+
end
|
256
|
+
|
257
|
+
def close!
|
258
|
+
@last_open_or_probed = 0
|
259
|
+
@opened_at_request_number = 0
|
260
|
+
@strategy.closed
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module AllMyCircuits
|
2
|
+
|
3
|
+
module Logging
|
4
|
+
def debug(message, breaker_name, request_number = nil)
|
5
|
+
AllMyCircuits.logger.debug(format_line(message, breaker_name, request_number))
|
6
|
+
end
|
7
|
+
|
8
|
+
def info(message, breaker_name, request_number = nil)
|
9
|
+
AllMyCircuits.logger.info(format_line(message, breaker_name, request_number))
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def format_line(message, breaker_name, request_number)
|
15
|
+
if request_number.nil?
|
16
|
+
"[%s] %s" % [breaker_name, message]
|
17
|
+
else
|
18
|
+
"[%s] req. #%s: %s" % [breaker_name, request_number, message]
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module AllMyCircuits
|
2
|
+
module Notifiers
|
3
|
+
|
4
|
+
# Public: notifies some service about change of breaker's state.
|
5
|
+
# For example, one could send a Librato metric whenever a circuit
|
6
|
+
# breaker is opened or closed.
|
7
|
+
#
|
8
|
+
class AbstractNotifier
|
9
|
+
def initialize(breaker_name, **kwargs)
|
10
|
+
@breaker_name = breaker_name
|
11
|
+
end
|
12
|
+
|
13
|
+
# Public: called once the circuit is tripped open.
|
14
|
+
#
|
15
|
+
def opened
|
16
|
+
raise NotImplementedError
|
17
|
+
end
|
18
|
+
|
19
|
+
# Public: called once the circuit is closed.
|
20
|
+
#
|
21
|
+
def closed
|
22
|
+
raise NotImplementedError
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module AllMyCircuits
|
2
|
+
module Notifiers
|
3
|
+
|
4
|
+
# Public: no-op implementation of AbstractNotifier.
|
5
|
+
#
|
6
|
+
class NullNotifier < AbstractNotifier
|
7
|
+
def initialize(*args, **kwargs)
|
8
|
+
end
|
9
|
+
|
10
|
+
def opened
|
11
|
+
end
|
12
|
+
|
13
|
+
def closed
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module AllMyCircuits
|
2
|
+
|
3
|
+
# Public: no-op circuit breaker implementation, useful for testing
|
4
|
+
#
|
5
|
+
class NullBreaker
|
6
|
+
attr_reader :name
|
7
|
+
attr_accessor :closed
|
8
|
+
|
9
|
+
def initialize(name:, closed:)
|
10
|
+
@name = name
|
11
|
+
@closed = closed
|
12
|
+
end
|
13
|
+
|
14
|
+
def run
|
15
|
+
if @closed
|
16
|
+
yield
|
17
|
+
else
|
18
|
+
raise AllMyCircuits::BreakerOpen, @name
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module AllMyCircuits
|
2
|
+
module Strategies
|
3
|
+
|
4
|
+
# Public: determines whether the circuit breaker should be tripped open
|
5
|
+
# upon another error within the supplied block.
|
6
|
+
#
|
7
|
+
# See AllMyCircuits::Strategies::NumberOverWindowStrategy,
|
8
|
+
# AllMyCircuits::Strategies::PercentageOverWindowStrategy for examples.
|
9
|
+
#
|
10
|
+
class AbstractStrategy
|
11
|
+
# Public: called whenever a request has ran successfully through circuit breaker.
|
12
|
+
#
|
13
|
+
def success
|
14
|
+
end
|
15
|
+
|
16
|
+
# Public: called whenever a request has failed within circuit breaker.
|
17
|
+
#
|
18
|
+
def error
|
19
|
+
end
|
20
|
+
|
21
|
+
# Public: called whenever circuit is tripped open.
|
22
|
+
#
|
23
|
+
def opened
|
24
|
+
end
|
25
|
+
|
26
|
+
# Public: called whenever circuit is closed.
|
27
|
+
#
|
28
|
+
def closed
|
29
|
+
end
|
30
|
+
|
31
|
+
# Public: called after each error within circuit breaker to determine
|
32
|
+
# whether it should be tripped open.
|
33
|
+
#
|
34
|
+
def should_open?
|
35
|
+
raise NotImplementedError
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module AllMyCircuits
|
2
|
+
module Strategies
|
3
|
+
class AbstractWindowStrategy
|
4
|
+
|
5
|
+
class Window
|
6
|
+
def initialize(number_of_events)
|
7
|
+
number_of_events = Integer(number_of_events)
|
8
|
+
unless number_of_events > 0
|
9
|
+
raise ArgumentError, "window size must be a natural number"
|
10
|
+
end
|
11
|
+
@number_of_events = number_of_events
|
12
|
+
reset!
|
13
|
+
end
|
14
|
+
|
15
|
+
def reset!
|
16
|
+
@window = []
|
17
|
+
@counters = Hash.new { |h, k| h[k] = 0 }
|
18
|
+
end
|
19
|
+
|
20
|
+
def <<(event)
|
21
|
+
if full?
|
22
|
+
event_to_decrement = @window.shift
|
23
|
+
@counters[event_to_decrement] -= 1
|
24
|
+
end
|
25
|
+
@window.push(event)
|
26
|
+
@counters[event] += 1
|
27
|
+
self
|
28
|
+
end
|
29
|
+
|
30
|
+
def count(event = nil)
|
31
|
+
event.nil? ? @window.length : @counters[event]
|
32
|
+
end
|
33
|
+
|
34
|
+
def full?
|
35
|
+
@window.length == @number_of_events
|
36
|
+
end
|
37
|
+
|
38
|
+
def inspect
|
39
|
+
"#<%s:0x%x size: %d, counts: %s, full: %s" % [self.class.name, object_id, @number_of_events, @counters, full?]
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module AllMyCircuits
|
2
|
+
module Strategies
|
3
|
+
|
4
|
+
class AbstractWindowStrategy < AbstractStrategy
|
5
|
+
autoload :Window, "all_my_circuits/strategies/abstract_window_strategy/window"
|
6
|
+
|
7
|
+
def initialize(requests_window:)
|
8
|
+
@requests_window = requests_window
|
9
|
+
@window = Window.new(@requests_window)
|
10
|
+
end
|
11
|
+
|
12
|
+
def success
|
13
|
+
@window << :succeeded
|
14
|
+
end
|
15
|
+
|
16
|
+
def error
|
17
|
+
@window << :failed
|
18
|
+
end
|
19
|
+
|
20
|
+
def opened
|
21
|
+
end
|
22
|
+
|
23
|
+
def closed
|
24
|
+
@window.reset!
|
25
|
+
end
|
26
|
+
|
27
|
+
def should_open?
|
28
|
+
raise NotImplementedError
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module AllMyCircuits
|
2
|
+
module Strategies
|
3
|
+
|
4
|
+
# Public: opens the circuit whenever failures threshold is reached
|
5
|
+
# within the window. Threshold is represented by absolute number of
|
6
|
+
# failures within the window.
|
7
|
+
#
|
8
|
+
class NumberOverWindowStrategy < AbstractWindowStrategy
|
9
|
+
|
10
|
+
# Public: initializes a new instance.
|
11
|
+
#
|
12
|
+
# Options
|
13
|
+
#
|
14
|
+
# requests_window - number of consecutive requests tracked by the window.
|
15
|
+
# failures_threshold - number of failures within the window after which
|
16
|
+
# the circuit is tripped open.
|
17
|
+
#
|
18
|
+
def initialize(failures_threshold:, **kwargs)
|
19
|
+
@failures_threshold = failures_threshold
|
20
|
+
super(**kwargs)
|
21
|
+
end
|
22
|
+
|
23
|
+
def should_open?
|
24
|
+
return unless @window.full?
|
25
|
+
|
26
|
+
failures = @window.count(:failed)
|
27
|
+
failures >= @failures_threshold
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module AllMyCircuits
|
2
|
+
module Strategies
|
3
|
+
|
4
|
+
# Public: opens the circuit whenever failures threshold is reached
|
5
|
+
# within the window. Threshold is represented by a percentage of
|
6
|
+
# failures within the window.
|
7
|
+
#
|
8
|
+
class PercentageOverWindowStrategy < AbstractWindowStrategy
|
9
|
+
|
10
|
+
# Public: initializes a new instance.
|
11
|
+
#
|
12
|
+
# Options
|
13
|
+
#
|
14
|
+
# requests_window - number of consecutive requests tracked by the window.
|
15
|
+
# failure_rate_percent_threshold - percent rate of failures within the window after which
|
16
|
+
# the circuit is tripped open.
|
17
|
+
#
|
18
|
+
def initialize(failure_rate_percent_threshold:, **kwargs)
|
19
|
+
@failure_rate_percent_threshold = failure_rate_percent_threshold
|
20
|
+
super(**kwargs)
|
21
|
+
end
|
22
|
+
|
23
|
+
def should_open?
|
24
|
+
return false unless @window.full?
|
25
|
+
|
26
|
+
failure_rate_percent = ((@window.count(:failed).to_f / @window.count) * 100).ceil
|
27
|
+
failure_rate_percent >= @failure_rate_percent_threshold
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module AllMyCircuits
|
2
|
+
|
3
|
+
module Strategies
|
4
|
+
autoload :AbstractStrategy, "all_my_circuits/strategies/abstract_strategy"
|
5
|
+
autoload :AbstractWindowStrategy, "all_my_circuits/strategies/abstract_window_strategy"
|
6
|
+
autoload :PercentageOverWindowStrategy, "all_my_circuits/strategies/percentage_over_window_strategy"
|
7
|
+
autoload :NumberOverWindowStrategy, "all_my_circuits/strategies/number_over_window_strategy"
|
8
|
+
end
|
9
|
+
|
10
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require "all_my_circuits/version"
|
2
|
+
require "logger"
|
3
|
+
|
4
|
+
module AllMyCircuits
|
5
|
+
require "all_my_circuits/exceptions"
|
6
|
+
|
7
|
+
autoload :Breaker, "all_my_circuits/breaker"
|
8
|
+
autoload :Clock, "all_my_circuits/clock"
|
9
|
+
autoload :Logging, "all_my_circuits/logging"
|
10
|
+
autoload :Notifiers, "all_my_circuits/notifiers"
|
11
|
+
autoload :NullBreaker, "all_my_circuits/null_breaker"
|
12
|
+
autoload :Strategies, "all_my_circuits/strategies"
|
13
|
+
autoload :VERSION, "all_my_circuits/version"
|
14
|
+
|
15
|
+
class << self
|
16
|
+
attr_accessor :logger
|
17
|
+
end
|
18
|
+
@logger = Logger.new(STDERR)
|
19
|
+
@logger.level = Integer(ENV["ALL_MY_CIRCUITS_LOG_LEVEL"]) rescue Logger::ERROR
|
20
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "rack"
|
4
|
+
require "rack/handler/puma"
|
5
|
+
require "rack/builder"
|
6
|
+
|
7
|
+
class FakeService
|
8
|
+
attr_accessor :state
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@random = Random.new
|
12
|
+
@state = :up
|
13
|
+
end
|
14
|
+
|
15
|
+
def call(env)
|
16
|
+
case @state
|
17
|
+
when :up
|
18
|
+
sleep @random.rand(0.100)
|
19
|
+
[200, { "Content-Type" => "text/html" }, ["up"]]
|
20
|
+
when :die
|
21
|
+
sleep @random.rand(1.000)
|
22
|
+
[500, { "Content-Type" => "text/html" }, ["down"]]
|
23
|
+
when :slowdown
|
24
|
+
sleep @random.rand(5.000)
|
25
|
+
[200, { "Content-Type" => "text/html" }, ["slooow"]]
|
26
|
+
else
|
27
|
+
fail "what is that again?"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
$service = FakeService.new
|
33
|
+
|
34
|
+
def set_state_action(state)
|
35
|
+
lambda { |_| $service.state = state; [200, { "Content-Type" => "text/html" }, ["OK"]] }
|
36
|
+
end
|
37
|
+
|
38
|
+
app = Rack::Builder.new do
|
39
|
+
map("/" ) { run $service }
|
40
|
+
map("/die") { run set_state_action(:die) }
|
41
|
+
map("/slowdown") { run set_state_action(:slowdown) }
|
42
|
+
map("/up") { run set_state_action(:up) }
|
43
|
+
end
|
44
|
+
|
45
|
+
server = ::Rack::Handler::Puma
|
46
|
+
server.run app, Port: 8081
|
@@ -0,0 +1,126 @@
|
|
1
|
+
require "rack"
|
2
|
+
require "rack/handler/puma"
|
3
|
+
require "json"
|
4
|
+
|
5
|
+
class Stream
|
6
|
+
def initialize(data_queue)
|
7
|
+
@data_queue = data_queue
|
8
|
+
end
|
9
|
+
|
10
|
+
def each
|
11
|
+
batch_proto = { succeeded: 0, failed: 0, skipped: 0 }.freeze
|
12
|
+
next_batch = batch_proto.dup
|
13
|
+
loop do
|
14
|
+
data_batch = next_batch
|
15
|
+
next_batch = batch_proto.dup
|
16
|
+
frame_end = Time.now + 1
|
17
|
+
begin
|
18
|
+
while r = @data_queue.pop(true)
|
19
|
+
if r[:finished] <= frame_end
|
20
|
+
data_batch[r[:status]] += 1
|
21
|
+
else
|
22
|
+
next_batch[r[:status]] += 1
|
23
|
+
break
|
24
|
+
end
|
25
|
+
end
|
26
|
+
rescue ThreadError # queue exhausted
|
27
|
+
sleep 0.0001
|
28
|
+
retry
|
29
|
+
end
|
30
|
+
data_batch[:currentTime] = frame_end.to_f * 1000
|
31
|
+
serialized = "data: %s\n\n" % data_batch.to_json
|
32
|
+
yield serialized
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
class GraphingServer
|
38
|
+
def self.start(data_queue)
|
39
|
+
server = ::Rack::Handler::Puma
|
40
|
+
server.run Rack::Chunked.new(new(Stream.new(data_queue)))
|
41
|
+
rescue => e
|
42
|
+
warn "graphing server wtf #{e.inspect} #{e.backtrace}"
|
43
|
+
raise
|
44
|
+
end
|
45
|
+
|
46
|
+
PAGE = <<-PAGE
|
47
|
+
<!DOCTYPE html>
|
48
|
+
<html>
|
49
|
+
<head>
|
50
|
+
<title>AllMyCircuits Stress Test Graph</title>
|
51
|
+
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.4/jquery.min.js" type="text/javascript"></script>
|
52
|
+
<script src="https://rawgit.com/joewalnes/smoothie/master/smoothie.js" type="text/javascript"></script>
|
53
|
+
<style type="text/css">
|
54
|
+
html{
|
55
|
+
height: 100%;
|
56
|
+
}
|
57
|
+
body {
|
58
|
+
min-height: 100%;
|
59
|
+
}
|
60
|
+
</style>
|
61
|
+
</head>
|
62
|
+
<body>
|
63
|
+
<canvas id="workersSuccess" width="1000" height="300"></canvas>
|
64
|
+
<canvas id="workersFailure" width="1000" height="300"></canvas>
|
65
|
+
<canvas id="workersSkip" width="1000" height="300"></canvas>
|
66
|
+
<script type="text/javascript">
|
67
|
+
var chartHeight = window.innerHeight / 3 - 20;
|
68
|
+
var chartWidth = window.innerWidth - 20;
|
69
|
+
|
70
|
+
var charts = {
|
71
|
+
succeeded: {
|
72
|
+
elementId: "workersSuccess",
|
73
|
+
color: "rgb(0, 255, 0)"
|
74
|
+
},
|
75
|
+
failed: {
|
76
|
+
elementId: "workersFailure",
|
77
|
+
color: "rgb(255, 0, 0)"
|
78
|
+
},
|
79
|
+
skipped: {
|
80
|
+
elementId: "workersSkip",
|
81
|
+
color: "rgb(255, 255, 255)"
|
82
|
+
}
|
83
|
+
};
|
84
|
+
|
85
|
+
for (var chart in charts) {
|
86
|
+
var chartConfig = charts[chart];
|
87
|
+
var el = document.getElementById(chartConfig.elementId);
|
88
|
+
el.height = chartHeight;
|
89
|
+
el.width = chartWidth;
|
90
|
+
|
91
|
+
var chart = new SmoothieChart({ interpolation: 'step', minValue: 0, maxValueScale: 1.2 });
|
92
|
+
chart.streamTo(el, 1000);
|
93
|
+
|
94
|
+
chartConfig.series = new TimeSeries();
|
95
|
+
chart.addTimeSeries(chartConfig.series, { strokeStyle: chartConfig.color });
|
96
|
+
}
|
97
|
+
|
98
|
+
var source = new EventSource("/data");
|
99
|
+
source.onmessage = function(event) {
|
100
|
+
var points = JSON.parse(event.data);
|
101
|
+
var currentTime = points.currentTime;
|
102
|
+
charts.succeeded.series.append(currentTime, points.succeeded);
|
103
|
+
charts.failed.series.append(currentTime, points.failed);
|
104
|
+
charts.skipped.series.append(currentTime, points.skipped);
|
105
|
+
};
|
106
|
+
</script>
|
107
|
+
</body>
|
108
|
+
</html>
|
109
|
+
PAGE
|
110
|
+
|
111
|
+
def initialize(stream)
|
112
|
+
@stream = stream
|
113
|
+
end
|
114
|
+
|
115
|
+
def call(env)
|
116
|
+
req = Rack::Request.new(env)
|
117
|
+
case req.path
|
118
|
+
when "/"
|
119
|
+
[200, { "Content-Type" => "text/html" }, [PAGE]]
|
120
|
+
when "/data"
|
121
|
+
[200, { "Content-Type" => "text/event-stream", "Connection" => "keepalive", "Cache-Control" => "no-cache, no-store" }, @stream]
|
122
|
+
else
|
123
|
+
[404, { "Content-Type" => "text/html" }, []]
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$:.unshift File.expand_path("../../lib", __FILE__)
|
4
|
+
require "all_my_circuits"
|
5
|
+
require_relative "./graphing_server"
|
6
|
+
require "thread"
|
7
|
+
require "net/http"
|
8
|
+
require "uri"
|
9
|
+
require "timeout"
|
10
|
+
require "securerandom"
|
11
|
+
|
12
|
+
WORKERS = Integer(ENV["WORKERS"]) rescue 25
|
13
|
+
|
14
|
+
def setup
|
15
|
+
@responses_queue = Queue.new
|
16
|
+
@breaker = AllMyCircuits::Breaker.new(
|
17
|
+
name: "test service circuit breaker",
|
18
|
+
sleep_seconds: 5,
|
19
|
+
strategy: AllMyCircuits::Strategies::PercentageOverWindowStrategy.new(
|
20
|
+
requests_window: 40,
|
21
|
+
failure_rate_percent_threshold: 25
|
22
|
+
)
|
23
|
+
)
|
24
|
+
@datapoints_queue = Queue.new
|
25
|
+
@commands_queue = Queue.new
|
26
|
+
end
|
27
|
+
|
28
|
+
def run
|
29
|
+
setup
|
30
|
+
workers = run_workers
|
31
|
+
graphing_server = run_graphing_server
|
32
|
+
|
33
|
+
workers.each(&:join)
|
34
|
+
graphing_server.join
|
35
|
+
end
|
36
|
+
|
37
|
+
def run_workers
|
38
|
+
WORKERS.times.map do
|
39
|
+
Thread.new(@responses_queue, @breaker) do |responses, breaker|
|
40
|
+
loop do
|
41
|
+
begin
|
42
|
+
t1 = Time.now
|
43
|
+
@breaker.run do
|
44
|
+
uri = URI("http://localhost:8081")
|
45
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
46
|
+
http.open_timeout = 2
|
47
|
+
http.read_timeout = 2
|
48
|
+
http.start do |h|
|
49
|
+
request = Net::HTTP::Get.new(uri)
|
50
|
+
response = h.request(request)
|
51
|
+
response.value
|
52
|
+
log "success"
|
53
|
+
responses.push(status: :succeeded, started: t1, finished: Time.now)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
rescue AllMyCircuits::BreakerOpen
|
57
|
+
log "breaker open"
|
58
|
+
responses.push(status: :skipped, started: t1, finished: Time.now)
|
59
|
+
rescue
|
60
|
+
log "failure #{$!.inspect}: #{$!.backtrace.first(2).join(", ")}"
|
61
|
+
responses.push(status: :failed, started: t1, finished: Time.now)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def run_graphing_server
|
69
|
+
Thread.new(@responses_queue) do |responses_queue|
|
70
|
+
GraphingServer.start(responses_queue)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def log(msg)
|
75
|
+
@mtx ||= Mutex.new
|
76
|
+
timestamp = "%10.6f" % Time.now.to_f
|
77
|
+
@mtx.synchronize { puts "[#{Thread.current.object_id}] #{timestamp}: #{msg}" if ENV["DEBUG"] }
|
78
|
+
end
|
79
|
+
|
80
|
+
run
|
metadata
ADDED
@@ -0,0 +1,142 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: all_my_circuits
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Vlad Yarotsky
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-05-29 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: concurrent-ruby
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0.8'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0.8'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.9'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.9'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '10.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '10.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: minitest
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '5.6'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '5.6'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: puma
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '2.11'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '2.11'
|
83
|
+
description: ''
|
84
|
+
email:
|
85
|
+
- vlad@remind101.com
|
86
|
+
executables: []
|
87
|
+
extensions: []
|
88
|
+
extra_rdoc_files: []
|
89
|
+
files:
|
90
|
+
- ".gitignore"
|
91
|
+
- Gemfile
|
92
|
+
- LICENSE
|
93
|
+
- README.md
|
94
|
+
- Rakefile
|
95
|
+
- all_my_circuits.gemspec
|
96
|
+
- all_my_circuits.jpg
|
97
|
+
- circle.yml
|
98
|
+
- lib/all_my_circuits.rb
|
99
|
+
- lib/all_my_circuits/breaker.rb
|
100
|
+
- lib/all_my_circuits/clock.rb
|
101
|
+
- lib/all_my_circuits/exceptions.rb
|
102
|
+
- lib/all_my_circuits/logging.rb
|
103
|
+
- lib/all_my_circuits/notifiers.rb
|
104
|
+
- lib/all_my_circuits/notifiers/abstract_notifier.rb
|
105
|
+
- lib/all_my_circuits/notifiers/null_notifier.rb
|
106
|
+
- lib/all_my_circuits/null_breaker.rb
|
107
|
+
- lib/all_my_circuits/strategies.rb
|
108
|
+
- lib/all_my_circuits/strategies/abstract_strategy.rb
|
109
|
+
- lib/all_my_circuits/strategies/abstract_window_strategy.rb
|
110
|
+
- lib/all_my_circuits/strategies/abstract_window_strategy/window.rb
|
111
|
+
- lib/all_my_circuits/strategies/number_over_window_strategy.rb
|
112
|
+
- lib/all_my_circuits/strategies/percentage_over_window_strategy.rb
|
113
|
+
- lib/all_my_circuits/version.rb
|
114
|
+
- script/fake_service.rb
|
115
|
+
- script/graphing_server.rb
|
116
|
+
- script/graphing_stress_test.rb
|
117
|
+
homepage: https://github.com/remind101/all_my_circuits
|
118
|
+
licenses:
|
119
|
+
- BSD
|
120
|
+
metadata: {}
|
121
|
+
post_install_message:
|
122
|
+
rdoc_options: []
|
123
|
+
require_paths:
|
124
|
+
- lib
|
125
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
126
|
+
requirements:
|
127
|
+
- - ">="
|
128
|
+
- !ruby/object:Gem::Version
|
129
|
+
version: '0'
|
130
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
131
|
+
requirements:
|
132
|
+
- - ">="
|
133
|
+
- !ruby/object:Gem::Version
|
134
|
+
version: '0'
|
135
|
+
requirements: []
|
136
|
+
rubyforge_project:
|
137
|
+
rubygems_version: 2.4.5
|
138
|
+
signing_key:
|
139
|
+
specification_version: 4
|
140
|
+
summary: Circuit Breaker library with support for rolling-window absolute/percentage
|
141
|
+
thresholds
|
142
|
+
test_files: []
|