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.
Files changed (38) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +56 -187
  3. data/lib/circuitbox.rb +14 -57
  4. data/lib/circuitbox/circuit_breaker.rb +137 -161
  5. data/lib/circuitbox/circuit_breaker/logger_messages.rb +31 -0
  6. data/lib/circuitbox/configuration.rb +51 -0
  7. data/lib/circuitbox/errors/error.rb +3 -2
  8. data/lib/circuitbox/errors/open_circuit_error.rb +3 -1
  9. data/lib/circuitbox/errors/service_failure_error.rb +5 -1
  10. data/lib/circuitbox/excon_middleware.rb +23 -30
  11. data/lib/circuitbox/faraday_middleware.rb +43 -63
  12. data/lib/circuitbox/memory_store.rb +85 -0
  13. data/lib/circuitbox/memory_store/container.rb +30 -0
  14. data/lib/circuitbox/memory_store/monotonic_time.rb +13 -0
  15. data/lib/circuitbox/notifier/active_support.rb +19 -0
  16. data/lib/circuitbox/notifier/null.rb +13 -0
  17. data/lib/circuitbox/timer.rb +51 -0
  18. data/lib/circuitbox/version.rb +3 -1
  19. metadata +106 -118
  20. data/.gitignore +0 -20
  21. data/.ruby-version +0 -1
  22. data/.travis.yml +0 -9
  23. data/Gemfile +0 -6
  24. data/Rakefile +0 -18
  25. data/benchmark/circuit_store_benchmark.rb +0 -114
  26. data/circuitbox.gemspec +0 -48
  27. data/lib/circuitbox/memcache_store.rb +0 -31
  28. data/lib/circuitbox/notifier.rb +0 -34
  29. data/test/circuit_breaker_test.rb +0 -428
  30. data/test/circuitbox_test.rb +0 -45
  31. data/test/excon_middleware_test.rb +0 -131
  32. data/test/faraday_middleware_test.rb +0 -175
  33. data/test/integration/circuitbox_cross_process_open_test.rb +0 -56
  34. data/test/integration/faraday_middleware_test.rb +0 -78
  35. data/test/integration_helper.rb +0 -48
  36. data/test/notifier_test.rb +0 -21
  37. data/test/service_failure_error_test.rb +0 -23
  38. 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
- attr_accessor :service, :circuit_options, :exceptions, :partition,
4
- :logger, :circuit_store, :notifier
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: 300,
13
+ sleep_window: 90,
8
14
  volume_threshold: 5,
9
- error_threshold: 50,
10
- timeout_seconds: 1,
11
- time_window: 60,
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
- # `timeout_seconds` - seconds until it will timeout the request
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.circuit_store }
29
- @notifier = options.fetch(:notifier_class) { 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
- @exceptions = [Timeout::Error] if @exceptions.blank?
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) { defined?(Rails) ? Rails.logger : Logger.new(STDOUT) }
35
- @time_class = options.fetch(:time_class) { Time }
36
- sanitize_options
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.fetch(name) { DEFAULTS.fetch(name) }
50
+ value = circuit_options[name]
41
51
  value.is_a?(Proc) ? value.call : value
42
52
  end
43
53
 
44
- def run!(run_options = {})
45
- @partition = run_options.delete(:partition) # sorry for this hack.
46
-
54
+ def run(circuitbox_exceptions: true, &block)
47
55
  if open?
48
- logger.debug "[CIRCUIT] open: skipping #{service}"
49
- open! unless open_flag?
50
- raise Circuitbox::OpenCircuitError.new(service)
56
+ skipped!
57
+ raise Circuitbox::OpenCircuitError.new(service) if circuitbox_exceptions
51
58
  else
52
- close! if was_open?
53
- logger.debug "[CIRCUIT] closed: querying #{service}"
59
+ logger.debug(circuit_running_message)
54
60
 
55
61
  begin
56
- response = if exceptions.include? Timeout::Error
57
- timeout_seconds = run_options.fetch(:timeout_seconds) { option_value(:timeout_seconds) }
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 => exception
66
- logger.debug "[CIRCUIT] closed: detected #{service} failure"
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
- open! if half_open?
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
- return response
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
- if open_flag?
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 > 0
97
- failure_count.to_f / all_count.to_f * 100
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(:failure), raw: true).to_i
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(:success), raw: true).to_i
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(storage_key(:asleep))
97
+ circuit_store.delete(open_storage_key)
110
98
  end
111
99
 
112
- private
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
- ### BEGIN - all this is just here to produce a close notification
122
- def close!
123
- log_event :close
124
- circuit_store.delete(storage_key(:was_open))
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
- def was_open?
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 half_open!
137
- circuit_store.store(storage_key(:half_open), true)
110
+ def passed_volume_threshold?(failures, successes)
111
+ failures + successes >= option_value(:volume_threshold)
138
112
  end
139
113
 
140
- def open_flag?
141
- circuit_store[storage_key(:asleep)].present?
114
+ def passed_rate_threshold?(rate)
115
+ rate >= option_value(:error_threshold)
142
116
  end
143
117
 
144
- def half_open?
145
- circuit_store[storage_key(:half_open)].present?
146
- end
118
+ def half_open_failure
119
+ @state_change_mutex.synchronize do
120
+ return if open? || !half_open?
147
121
 
148
- def passed_volume_threshold?
149
- success_count + failure_count > option_value(:volume_threshold)
150
- end
122
+ trip
123
+ end
151
124
 
152
- def passed_rate_threshold?
153
- read_and_log_error_rate >= option_value(:error_threshold)
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 read_and_log_error_rate
157
- failures = failure_count
158
- success = success_count
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
- def success!
165
- log_event :success
166
- circuit_store.delete(storage_key(:half_open))
167
- end
134
+ trip
135
+ end
168
136
 
169
- def failure!
170
- log_event :failure
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
- # Store success/failure/open/close data in memcache
174
- def log_event(event)
175
- notifier.new(service,partition).notify(event)
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 log_metrics(error_rate, failures, successes)
180
- n = notifier.new(service,partition)
181
- n.metric_gauge(:error_rate, error_rate)
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 sanitize_options
187
- sleep_window = option_value(:sleep_window)
188
- time_window = option_value(:time_window)
189
- if sleep_window < time_window
190
- notifier.new(service,partition).notify_warning("sleep_window:#{sleep_window} is shorter than time_window:#{time_window}, the error_rate could not be reset properly after a sleep. sleep_window as been set to equal time_window.")
191
- @circuit_options[:sleep_window] = option_value(:time_window)
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
- # When there is a successful response within a count interval, clear the failures.
196
- def clear_failures!
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
- # Logs to process memory.
201
- def log_event_to_process(event)
202
- key = stat_storage_key(event)
203
- if circuit_store.load(key, raw: true)
204
- circuit_store.increment(key)
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
- # Logs to Memcache.
212
- def log_event_to_stat_store(key)
213
- if stat_store.read(key, raw: true)
214
- stat_store.increment(key)
215
- else
216
- stat_store.store(key, 1)
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
- # For returning stale responses when the circuit is open
221
- def response_key(args)
222
- Digest::SHA1.hexdigest(storage_key(:cache, args.inspect.to_s))
188
+ def skipped!
189
+ notify_event('skipped')
190
+ logger.debug(circuit_skipped_message)
223
191
  end
224
192
 
225
- def stat_storage_key(event, options = {})
226
- storage_key(:stats, align_time_on_minute, event, options)
193
+ # Send event notification to notifier
194
+ def notify_event(event)
195
+ notifier.notify(service, event)
227
196
  end
228
197
 
229
-
230
- # return time representation in seconds
231
- def align_time_on_minute(time=nil)
232
- time ||= @time_class.now.to_i
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 storage_key(*args)
238
- options = args.extract_options!
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
- key = if options[:without_partition]
241
- "circuits:#{service}:#{args.join(":")}"
242
- else
243
- "circuits:#{service}:#{partition}:#{args.join(":")}"
244
- end
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
- return key
215
+ def stat_storage_key(event)
216
+ "circuits:#{service}:stats:#{align_time_to_window}:#{event}"
247
217
  end
248
218
 
249
- def timeout(timeout_seconds, &block)
250
- Timeout::timeout(timeout_seconds) { block.call }
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 self.reset
254
- Circuitbox.reset
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