circuitbox 1.1.0 → 2.0.0.pre4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/README.md +56 -187
- data/lib/circuitbox.rb +14 -57
- data/lib/circuitbox/circuit_breaker.rb +137 -161
- data/lib/circuitbox/circuit_breaker/logger_messages.rb +31 -0
- data/lib/circuitbox/configuration.rb +51 -0
- data/lib/circuitbox/errors/error.rb +3 -2
- data/lib/circuitbox/errors/open_circuit_error.rb +3 -1
- data/lib/circuitbox/errors/service_failure_error.rb +5 -1
- data/lib/circuitbox/excon_middleware.rb +23 -30
- data/lib/circuitbox/faraday_middleware.rb +43 -63
- data/lib/circuitbox/memory_store.rb +85 -0
- data/lib/circuitbox/memory_store/container.rb +30 -0
- data/lib/circuitbox/memory_store/monotonic_time.rb +13 -0
- data/lib/circuitbox/notifier/active_support.rb +19 -0
- data/lib/circuitbox/notifier/null.rb +13 -0
- data/lib/circuitbox/timer.rb +51 -0
- data/lib/circuitbox/version.rb +3 -1
- metadata +106 -118
- data/.gitignore +0 -20
- data/.ruby-version +0 -1
- data/.travis.yml +0 -9
- data/Gemfile +0 -6
- data/Rakefile +0 -18
- data/benchmark/circuit_store_benchmark.rb +0 -114
- data/circuitbox.gemspec +0 -48
- data/lib/circuitbox/memcache_store.rb +0 -31
- data/lib/circuitbox/notifier.rb +0 -34
- data/test/circuit_breaker_test.rb +0 -428
- data/test/circuitbox_test.rb +0 -45
- data/test/excon_middleware_test.rb +0 -131
- data/test/faraday_middleware_test.rb +0 -175
- data/test/integration/circuitbox_cross_process_open_test.rb +0 -56
- data/test/integration/faraday_middleware_test.rb +0 -78
- data/test/integration_helper.rb +0 -48
- data/test/notifier_test.rb +0 -21
- data/test/service_failure_error_test.rb +0 -23
- data/test/test_helper.rb +0 -15
@@ -1,15 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'circuit_breaker/logger_messages'
|
4
|
+
|
1
5
|
class Circuitbox
|
2
6
|
class CircuitBreaker
|
3
|
-
|
4
|
-
|
7
|
+
include LoggerMessages
|
8
|
+
|
9
|
+
attr_reader :service, :circuit_options, :exceptions,
|
10
|
+
:logger, :circuit_store, :notifier, :time_class
|
5
11
|
|
6
12
|
DEFAULTS = {
|
7
|
-
sleep_window:
|
13
|
+
sleep_window: 90,
|
8
14
|
volume_threshold: 5,
|
9
|
-
error_threshold:
|
10
|
-
|
11
|
-
|
12
|
-
}
|
15
|
+
error_threshold: 50,
|
16
|
+
time_window: 60
|
17
|
+
}.freeze
|
13
18
|
|
14
19
|
#
|
15
20
|
# Configuration options
|
@@ -17,242 +22,213 @@ class Circuitbox
|
|
17
22
|
# `sleep_window` - seconds to sleep the circuit
|
18
23
|
# `volume_threshold` - number of requests before error rate calculation occurs
|
19
24
|
# `error_threshold` - percentage of failed requests needed to trip circuit
|
20
|
-
# `
|
21
|
-
# `exceptions` - exceptions other than Timeout::Error that count as failures
|
25
|
+
# `exceptions` - exceptions that count as failures
|
22
26
|
# `time_window` - interval of time used to calculate error_rate (in seconds) - default is 60s
|
23
27
|
# `logger` - Logger to use - defaults to Rails.logger if defined, otherwise STDOUT
|
24
28
|
#
|
25
29
|
def initialize(service, options = {})
|
26
|
-
@service = service
|
27
|
-
@circuit_options = options
|
28
|
-
@circuit_store = options.fetch(:cache) { Circuitbox.
|
29
|
-
@notifier
|
30
|
+
@service = service.to_s
|
31
|
+
@circuit_options = DEFAULTS.merge(options)
|
32
|
+
@circuit_store = options.fetch(:cache) { Circuitbox.default_circuit_store }
|
33
|
+
@notifier = options.fetch(:notifier) { Circuitbox.default_notifier }
|
34
|
+
|
35
|
+
if @circuit_options[:timeout_seconds]
|
36
|
+
warn('timeout_seconds was removed in circuitbox 2.0. '\
|
37
|
+
'Check the upgrade guide at https://github.com/yammer/circuitbox')
|
38
|
+
end
|
30
39
|
|
31
|
-
@exceptions = options.fetch(:exceptions)
|
32
|
-
|
40
|
+
@exceptions = options.fetch(:exceptions)
|
41
|
+
raise ArgumentError.new('exceptions need to be an array') unless @exceptions.is_a?(Array)
|
33
42
|
|
34
|
-
@logger = options.fetch(:logger) {
|
35
|
-
@time_class
|
36
|
-
|
43
|
+
@logger = options.fetch(:logger) { Circuitbox.default_logger }
|
44
|
+
@time_class = options.fetch(:time_class) { Time }
|
45
|
+
@state_change_mutex = Mutex.new
|
46
|
+
check_sleep_window
|
37
47
|
end
|
38
48
|
|
39
49
|
def option_value(name)
|
40
|
-
value = circuit_options
|
50
|
+
value = circuit_options[name]
|
41
51
|
value.is_a?(Proc) ? value.call : value
|
42
52
|
end
|
43
53
|
|
44
|
-
def run
|
45
|
-
@partition = run_options.delete(:partition) # sorry for this hack.
|
46
|
-
|
54
|
+
def run(circuitbox_exceptions: true, &block)
|
47
55
|
if open?
|
48
|
-
|
49
|
-
|
50
|
-
raise Circuitbox::OpenCircuitError.new(service)
|
56
|
+
skipped!
|
57
|
+
raise Circuitbox::OpenCircuitError.new(service) if circuitbox_exceptions
|
51
58
|
else
|
52
|
-
|
53
|
-
logger.debug "[CIRCUIT] closed: querying #{service}"
|
59
|
+
logger.debug(circuit_running_message)
|
54
60
|
|
55
61
|
begin
|
56
|
-
response =
|
57
|
-
|
58
|
-
timeout (timeout_seconds) { yield }
|
59
|
-
else
|
60
|
-
yield
|
61
|
-
end
|
62
|
-
|
63
|
-
logger.debug "[CIRCUIT] closed: #{service} querie success"
|
62
|
+
response = Timer.measure(service, notifier, 'runtime', &block)
|
63
|
+
|
64
64
|
success!
|
65
|
-
rescue *exceptions =>
|
66
|
-
|
65
|
+
rescue *exceptions => e
|
66
|
+
# Other stores could raise an exception that circuitbox is asked to watch.
|
67
|
+
# setting to nil keeps the same behavior as the previous defination of run.
|
68
|
+
response = nil
|
67
69
|
failure!
|
68
|
-
|
69
|
-
raise Circuitbox::ServiceFailureError.new(service, exception)
|
70
|
+
raise Circuitbox::ServiceFailureError.new(service, e) if circuitbox_exceptions
|
70
71
|
end
|
71
72
|
end
|
72
73
|
|
73
|
-
|
74
|
-
end
|
75
|
-
|
76
|
-
def run(run_options = {})
|
77
|
-
begin
|
78
|
-
run!(run_options, &Proc.new)
|
79
|
-
rescue Circuitbox::Error
|
80
|
-
nil
|
81
|
-
end
|
74
|
+
response
|
82
75
|
end
|
83
76
|
|
84
77
|
def open?
|
85
|
-
|
86
|
-
true
|
87
|
-
elsif passed_volume_threshold? && passed_rate_threshold?
|
88
|
-
true
|
89
|
-
else
|
90
|
-
false
|
91
|
-
end
|
78
|
+
circuit_store.key?(open_storage_key)
|
92
79
|
end
|
93
80
|
|
94
81
|
def error_rate(failures = failure_count, success = success_count)
|
95
82
|
all_count = failures + success
|
96
|
-
return 0.0 unless all_count
|
97
|
-
|
83
|
+
return 0.0 unless all_count.positive?
|
84
|
+
|
85
|
+
(failures / all_count.to_f) * 100
|
98
86
|
end
|
99
87
|
|
100
88
|
def failure_count
|
101
|
-
circuit_store.load(stat_storage_key(
|
89
|
+
circuit_store.load(stat_storage_key('failure'), raw: true).to_i
|
102
90
|
end
|
103
91
|
|
104
92
|
def success_count
|
105
|
-
circuit_store.load(stat_storage_key(
|
93
|
+
circuit_store.load(stat_storage_key('success'), raw: true).to_i
|
106
94
|
end
|
107
95
|
|
108
96
|
def try_close_next_time
|
109
|
-
circuit_store.delete(
|
97
|
+
circuit_store.delete(open_storage_key)
|
110
98
|
end
|
111
99
|
|
112
|
-
|
113
|
-
def open!
|
114
|
-
log_event :open
|
115
|
-
logger.debug "[CIRCUIT] opening #{service} circuit"
|
116
|
-
circuit_store.store(storage_key(:asleep), true, expires: option_value(:sleep_window))
|
117
|
-
half_open!
|
118
|
-
was_open!
|
119
|
-
end
|
100
|
+
private
|
120
101
|
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
end
|
126
|
-
|
127
|
-
def was_open!
|
128
|
-
circuit_store.store(storage_key(:was_open), true)
|
129
|
-
end
|
102
|
+
def should_open?
|
103
|
+
failures = failure_count
|
104
|
+
successes = success_count
|
105
|
+
rate = error_rate(failures, successes)
|
130
106
|
|
131
|
-
|
132
|
-
circuit_store[storage_key(:was_open)].present?
|
107
|
+
passed_volume_threshold?(failures, successes) && passed_rate_threshold?(rate)
|
133
108
|
end
|
134
|
-
### END
|
135
109
|
|
136
|
-
def
|
137
|
-
|
110
|
+
def passed_volume_threshold?(failures, successes)
|
111
|
+
failures + successes >= option_value(:volume_threshold)
|
138
112
|
end
|
139
113
|
|
140
|
-
def
|
141
|
-
|
114
|
+
def passed_rate_threshold?(rate)
|
115
|
+
rate >= option_value(:error_threshold)
|
142
116
|
end
|
143
117
|
|
144
|
-
def
|
145
|
-
|
146
|
-
|
118
|
+
def half_open_failure
|
119
|
+
@state_change_mutex.synchronize do
|
120
|
+
return if open? || !half_open?
|
147
121
|
|
148
|
-
|
149
|
-
|
150
|
-
end
|
122
|
+
trip
|
123
|
+
end
|
151
124
|
|
152
|
-
|
153
|
-
|
125
|
+
# Running event and logger outside of the synchronize block to allow other threads
|
126
|
+
# that may be waiting to become unblocked
|
127
|
+
notify_opened
|
154
128
|
end
|
155
129
|
|
156
|
-
def
|
157
|
-
|
158
|
-
|
159
|
-
rate = error_rate(failures, success)
|
160
|
-
log_metrics(rate, failures, success)
|
161
|
-
rate
|
162
|
-
end
|
130
|
+
def open!
|
131
|
+
@state_change_mutex.synchronize do
|
132
|
+
return if open?
|
163
133
|
|
164
|
-
|
165
|
-
|
166
|
-
circuit_store.delete(storage_key(:half_open))
|
167
|
-
end
|
134
|
+
trip
|
135
|
+
end
|
168
136
|
|
169
|
-
|
170
|
-
|
137
|
+
# Running event and logger outside of the synchronize block to allow other threads
|
138
|
+
# that may be waiting to become unblocked
|
139
|
+
notify_opened
|
171
140
|
end
|
172
141
|
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
log_event_to_process(event)
|
142
|
+
def notify_opened
|
143
|
+
notify_event('open')
|
144
|
+
logger.debug(circuit_opened_message)
|
177
145
|
end
|
178
146
|
|
179
|
-
def
|
180
|
-
|
181
|
-
|
182
|
-
n.metric_gauge(:failure_count, failures)
|
183
|
-
n.metric_gauge(:success_count, successes)
|
147
|
+
def trip
|
148
|
+
circuit_store.store(open_storage_key, true, expires: option_value(:sleep_window))
|
149
|
+
circuit_store.store(half_open_storage_key, true)
|
184
150
|
end
|
185
151
|
|
186
|
-
def
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
152
|
+
def close!
|
153
|
+
@state_change_mutex.synchronize do
|
154
|
+
# If the circuit is not open, the half_open key will be deleted from the store
|
155
|
+
# if half_open exists the deleted value is returned and allows us to continue
|
156
|
+
# if half_open doesn't exist nil is returned, causing us to return early
|
157
|
+
return unless !open? && circuit_store.delete(half_open_storage_key)
|
192
158
|
end
|
159
|
+
|
160
|
+
# Running event outside of the synchronize block to allow other threads
|
161
|
+
# that may be waiting to become unblocked
|
162
|
+
notify_event('close')
|
163
|
+
logger.debug(circuit_closed_message)
|
193
164
|
end
|
194
165
|
|
195
|
-
|
196
|
-
|
197
|
-
circuit_store.store(stat_storage_key(:failure), 0, raw: true)
|
166
|
+
def half_open?
|
167
|
+
circuit_store.key?(half_open_storage_key)
|
198
168
|
end
|
199
169
|
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
else
|
206
|
-
# yes we want a string here, as the underlying stores impement this as a native type.
|
207
|
-
circuit_store.store(key, "1", raw: true)
|
208
|
-
end
|
170
|
+
def success!
|
171
|
+
increment_and_notify_event('success')
|
172
|
+
logger.debug(circuit_success_message)
|
173
|
+
|
174
|
+
close! if half_open?
|
209
175
|
end
|
210
176
|
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
177
|
+
def failure!
|
178
|
+
increment_and_notify_event('failure')
|
179
|
+
logger.debug(circuit_failure_message)
|
180
|
+
|
181
|
+
if half_open?
|
182
|
+
half_open_failure
|
183
|
+
elsif should_open?
|
184
|
+
open!
|
217
185
|
end
|
218
186
|
end
|
219
187
|
|
220
|
-
|
221
|
-
|
222
|
-
|
188
|
+
def skipped!
|
189
|
+
notify_event('skipped')
|
190
|
+
logger.debug(circuit_skipped_message)
|
223
191
|
end
|
224
192
|
|
225
|
-
|
226
|
-
|
193
|
+
# Send event notification to notifier
|
194
|
+
def notify_event(event)
|
195
|
+
notifier.notify(service, event)
|
227
196
|
end
|
228
197
|
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
time_window = option_value(:time_window)
|
234
|
-
time - ( time % time_window ) # remove rest of integer division
|
198
|
+
# Increment stat store and send notification
|
199
|
+
def increment_and_notify_event(event)
|
200
|
+
circuit_store.increment(stat_storage_key(event), 1, expires: (option_value(:time_window) * 2))
|
201
|
+
notify_event(event)
|
235
202
|
end
|
236
203
|
|
237
|
-
def
|
238
|
-
|
204
|
+
def check_sleep_window
|
205
|
+
sleep_window = option_value(:sleep_window)
|
206
|
+
time_window = option_value(:time_window)
|
207
|
+
return unless sleep_window < time_window
|
239
208
|
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
209
|
+
warning_message = "sleep_window: #{sleep_window} is shorter than time_window: #{time_window}, "\
|
210
|
+
"the error_rate would not be reset after a sleep."
|
211
|
+
notifier.notify_warning(service, warning_message)
|
212
|
+
warn("Circuit: #{service}, Warning: #{warning_message}")
|
213
|
+
end
|
245
214
|
|
246
|
-
|
215
|
+
def stat_storage_key(event)
|
216
|
+
"circuits:#{service}:stats:#{align_time_to_window}:#{event}"
|
247
217
|
end
|
248
218
|
|
249
|
-
|
250
|
-
|
219
|
+
# return time representation in seconds
|
220
|
+
def align_time_to_window
|
221
|
+
time = time_class.now.to_i
|
222
|
+
time_window = option_value(:time_window)
|
223
|
+
time - (time % time_window) # remove rest of integer division
|
251
224
|
end
|
252
225
|
|
253
|
-
def
|
254
|
-
|
226
|
+
def open_storage_key
|
227
|
+
@open_storage_key ||= "circuits:#{service}:open"
|
255
228
|
end
|
256
229
|
|
230
|
+
def half_open_storage_key
|
231
|
+
@half_open_storage_key ||= "circuits:#{service}:half_open"
|
232
|
+
end
|
257
233
|
end
|
258
234
|
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Circuitbox
|
4
|
+
class CircuitBreaker
|
5
|
+
module LoggerMessages
|
6
|
+
def circuit_skipped_message
|
7
|
+
@circuit_skipped_message ||= "[CIRCUIT] #{service}: skipped"
|
8
|
+
end
|
9
|
+
|
10
|
+
def circuit_running_message
|
11
|
+
@circuit_running_message ||= "[CIRCUIT] #{service}: running"
|
12
|
+
end
|
13
|
+
|
14
|
+
def circuit_success_message
|
15
|
+
@circuit_success_message ||= "[CIRCUIT] #{service}: success"
|
16
|
+
end
|
17
|
+
|
18
|
+
def circuit_failure_message
|
19
|
+
@circuit_failure_message ||= "[CIRCUIT] #{service}: failure"
|
20
|
+
end
|
21
|
+
|
22
|
+
def circuit_opened_message
|
23
|
+
@circuit_opened_message ||= "[CIRCUIT] #{service}: opened"
|
24
|
+
end
|
25
|
+
|
26
|
+
def circuit_closed_message
|
27
|
+
@circuit_closed_message ||= "[CIRCUIT] #{service}: closed"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'memory_store'
|
4
|
+
require_relative 'timer'
|
5
|
+
require_relative 'notifier/active_support'
|
6
|
+
require_relative 'notifier/null'
|
7
|
+
|
8
|
+
class Circuitbox
|
9
|
+
module Configuration
|
10
|
+
attr_writer :default_circuit_store,
|
11
|
+
:default_notifier,
|
12
|
+
:default_timer,
|
13
|
+
:default_logger
|
14
|
+
|
15
|
+
def configure
|
16
|
+
yield self
|
17
|
+
clear_cached_circuits!
|
18
|
+
nil
|
19
|
+
end
|
20
|
+
|
21
|
+
def default_circuit_store
|
22
|
+
@default_circuit_store ||= MemoryStore.new
|
23
|
+
end
|
24
|
+
|
25
|
+
def default_notifier
|
26
|
+
@default_notifier ||= if defined?(ActiveSupport::Notifications)
|
27
|
+
Notifier::ActiveSupport.new
|
28
|
+
else
|
29
|
+
Notifier::Null.new
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def default_logger
|
34
|
+
@default_logger ||= if defined?(Rails)
|
35
|
+
Rails.logger
|
36
|
+
else
|
37
|
+
Logger.new($stdout)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def cached_circuits
|
44
|
+
@cached_circuits ||= {}
|
45
|
+
end
|
46
|
+
|
47
|
+
def clear_cached_circuits!
|
48
|
+
@cached_circuits = {}
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|