circuitbox 1.1.1 → 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 (37) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +53 -187
  3. data/lib/circuitbox/circuit_breaker/logger_messages.rb +31 -0
  4. data/lib/circuitbox/circuit_breaker.rb +134 -154
  5. data/lib/circuitbox/configuration.rb +51 -0
  6. data/lib/circuitbox/errors/error.rb +3 -2
  7. data/lib/circuitbox/errors/open_circuit_error.rb +3 -1
  8. data/lib/circuitbox/errors/service_failure_error.rb +5 -1
  9. data/lib/circuitbox/excon_middleware.rb +23 -30
  10. data/lib/circuitbox/faraday_middleware.rb +43 -63
  11. data/lib/circuitbox/memory_store/container.rb +30 -0
  12. data/lib/circuitbox/memory_store/monotonic_time.rb +13 -0
  13. data/lib/circuitbox/memory_store.rb +85 -0
  14. data/lib/circuitbox/notifier/active_support.rb +19 -0
  15. data/lib/circuitbox/notifier/null.rb +13 -0
  16. data/lib/circuitbox/timer.rb +51 -0
  17. data/lib/circuitbox/version.rb +3 -1
  18. data/lib/circuitbox.rb +14 -54
  19. metadata +106 -117
  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 -30
  25. data/benchmark/circuit_store_benchmark.rb +0 -114
  26. data/circuitbox.gemspec +0 -48
  27. data/lib/circuitbox/notifier.rb +0 -34
  28. data/test/circuit_breaker_test.rb +0 -436
  29. data/test/circuitbox_test.rb +0 -45
  30. data/test/excon_middleware_test.rb +0 -131
  31. data/test/faraday_middleware_test.rb +0 -175
  32. data/test/integration/circuitbox_cross_process_open_test.rb +0 -56
  33. data/test/integration/faraday_middleware_test.rb +0 -78
  34. data/test/integration_helper.rb +0 -48
  35. data/test/notifier_test.rb +0 -21
  36. data/test/service_failure_error_test.rb +0 -23
  37. 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,238 +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
56
  skipped!
51
- raise Circuitbox::OpenCircuitError.new(service)
57
+ raise Circuitbox::OpenCircuitError.new(service) if circuitbox_exceptions
52
58
  else
53
- close! if was_open?
54
- logger.debug "[CIRCUIT] closed: querying #{service}"
59
+ logger.debug(circuit_running_message)
55
60
 
56
61
  begin
57
- response = if exceptions.include? Timeout::Error
58
- timeout_seconds = run_options.fetch(:timeout_seconds) { option_value(:timeout_seconds) }
59
- timeout (timeout_seconds) { yield }
60
- else
61
- yield
62
- end
63
-
64
- logger.debug "[CIRCUIT] closed: #{service} querie success"
62
+ response = Timer.measure(service, notifier, 'runtime', &block)
63
+
65
64
  success!
66
- rescue *exceptions => exception
67
- 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
68
69
  failure!
69
- open! if half_open?
70
- raise Circuitbox::ServiceFailureError.new(service, exception)
70
+ raise Circuitbox::ServiceFailureError.new(service, e) if circuitbox_exceptions
71
71
  end
72
72
  end
73
73
 
74
- return response
75
- end
76
-
77
- def run(run_options = {})
78
- begin
79
- run!(run_options, &Proc.new)
80
- rescue Circuitbox::Error
81
- nil
82
- end
74
+ response
83
75
  end
84
76
 
85
77
  def open?
86
- if open_flag?
87
- true
88
- elsif passed_volume_threshold? && passed_rate_threshold?
89
- true
90
- else
91
- false
92
- end
78
+ circuit_store.key?(open_storage_key)
93
79
  end
94
80
 
95
81
  def error_rate(failures = failure_count, success = success_count)
96
82
  all_count = failures + success
97
- return 0.0 unless all_count > 0
98
- 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
99
86
  end
100
87
 
101
88
  def failure_count
102
- circuit_store.load(stat_storage_key(:failure), raw: true).to_i
89
+ circuit_store.load(stat_storage_key('failure'), raw: true).to_i
103
90
  end
104
91
 
105
92
  def success_count
106
- circuit_store.load(stat_storage_key(:success), raw: true).to_i
93
+ circuit_store.load(stat_storage_key('success'), raw: true).to_i
107
94
  end
108
95
 
109
96
  def try_close_next_time
110
- circuit_store.delete(storage_key(:asleep))
97
+ circuit_store.delete(open_storage_key)
111
98
  end
112
99
 
113
- private
114
- def open!
115
- log_event :open
116
- logger.debug "[CIRCUIT] opening #{service} circuit"
117
- circuit_store.store(storage_key(:asleep), true, expires: option_value(:sleep_window))
118
- half_open!
119
- was_open!
120
- end
100
+ private
121
101
 
122
- ### BEGIN - all this is just here to produce a close notification
123
- def close!
124
- log_event :close
125
- circuit_store.delete(storage_key(:was_open))
102
+ def should_open?
103
+ failures = failure_count
104
+ successes = success_count
105
+ rate = error_rate(failures, successes)
106
+
107
+ passed_volume_threshold?(failures, successes) && passed_rate_threshold?(rate)
126
108
  end
127
109
 
128
- def was_open!
129
- circuit_store.store(storage_key(:was_open), true)
110
+ def passed_volume_threshold?(failures, successes)
111
+ failures + successes >= option_value(:volume_threshold)
130
112
  end
131
113
 
132
- def was_open?
133
- circuit_store[storage_key(:was_open)].present?
114
+ def passed_rate_threshold?(rate)
115
+ rate >= option_value(:error_threshold)
134
116
  end
135
- ### END
136
117
 
137
- def half_open!
138
- circuit_store.store(storage_key(:half_open), true)
118
+ def half_open_failure
119
+ @state_change_mutex.synchronize do
120
+ return if open? || !half_open?
121
+
122
+ trip
123
+ end
124
+
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
139
128
  end
140
129
 
141
- def open_flag?
142
- circuit_store[storage_key(:asleep)].present?
130
+ def open!
131
+ @state_change_mutex.synchronize do
132
+ return if open?
133
+
134
+ trip
135
+ end
136
+
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
143
140
  end
144
141
 
145
- def half_open?
146
- circuit_store[storage_key(:half_open)].present?
142
+ def notify_opened
143
+ notify_event('open')
144
+ logger.debug(circuit_opened_message)
147
145
  end
148
146
 
149
- def passed_volume_threshold?
150
- success_count + failure_count > option_value(:volume_threshold)
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)
151
150
  end
152
151
 
153
- def passed_rate_threshold?
154
- read_and_log_error_rate >= option_value(:error_threshold)
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)
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)
155
164
  end
156
165
 
157
- def read_and_log_error_rate
158
- failures = failure_count
159
- success = success_count
160
- rate = error_rate(failures, success)
161
- log_metrics(rate, failures, success)
162
- rate
166
+ def half_open?
167
+ circuit_store.key?(half_open_storage_key)
163
168
  end
164
169
 
165
170
  def success!
166
- log_event :success
167
- circuit_store.delete(storage_key(:half_open))
171
+ increment_and_notify_event('success')
172
+ logger.debug(circuit_success_message)
173
+
174
+ close! if half_open?
168
175
  end
169
176
 
170
177
  def failure!
171
- log_event :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!
185
+ end
172
186
  end
173
187
 
174
188
  def skipped!
175
- log_event :skipped
189
+ notify_event('skipped')
190
+ logger.debug(circuit_skipped_message)
176
191
  end
177
192
 
178
- # Store success/failure/open/close data in memcache
179
- def log_event(event)
180
- notifier.new(service,partition).notify(event)
181
- log_event_to_process(event)
193
+ # Send event notification to notifier
194
+ def notify_event(event)
195
+ notifier.notify(service, event)
182
196
  end
183
197
 
184
- def log_metrics(error_rate, failures, successes)
185
- n = notifier.new(service,partition)
186
- n.metric_gauge(:error_rate, error_rate)
187
- n.metric_gauge(:failure_count, failures)
188
- n.metric_gauge(:success_count, successes)
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)
189
202
  end
190
203
 
191
- def sanitize_options
204
+ def check_sleep_window
192
205
  sleep_window = option_value(:sleep_window)
193
206
  time_window = option_value(:time_window)
194
- if sleep_window < time_window
195
- 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.")
196
- @circuit_options[:sleep_window] = option_value(:time_window)
197
- end
198
- end
199
-
200
- # When there is a successful response within a count interval, clear the failures.
201
- def clear_failures!
202
- circuit_store.store(stat_storage_key(:failure), 0, raw: true)
203
- end
207
+ return unless sleep_window < time_window
204
208
 
205
- # Logs to process memory.
206
- def log_event_to_process(event)
207
- key = stat_storage_key(event)
208
- if circuit_store.load(key, raw: true)
209
- circuit_store.increment(key)
210
- else
211
- # yes we want a string here, as the underlying stores impement this as a native type.
212
- circuit_store.store(key, "1", raw: true)
213
- end
214
- end
215
-
216
- # For returning stale responses when the circuit is open
217
- def response_key(args)
218
- Digest::SHA1.hexdigest(storage_key(:cache, args.inspect.to_s))
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}")
219
213
  end
220
214
 
221
- def stat_storage_key(event, options = {})
222
- storage_key(:stats, align_time_on_minute, event, options)
215
+ def stat_storage_key(event)
216
+ "circuits:#{service}:stats:#{align_time_to_window}:#{event}"
223
217
  end
224
218
 
225
-
226
219
  # return time representation in seconds
227
- def align_time_on_minute(time=nil)
228
- time ||= @time_class.now.to_i
220
+ def align_time_to_window
221
+ time = time_class.now.to_i
229
222
  time_window = option_value(:time_window)
230
- time - ( time % time_window ) # remove rest of integer division
231
- end
232
-
233
- def storage_key(*args)
234
- options = args.extract_options!
235
-
236
- key = if options[:without_partition]
237
- "circuits:#{service}:#{args.join(":")}"
238
- else
239
- "circuits:#{service}:#{partition}:#{args.join(":")}"
240
- end
241
-
242
- return key
223
+ time - (time % time_window) # remove rest of integer division
243
224
  end
244
225
 
245
- def timeout(timeout_seconds, &block)
246
- Timeout::timeout(timeout_seconds) { block.call }
226
+ def open_storage_key
227
+ @open_storage_key ||= "circuits:#{service}:open"
247
228
  end
248
229
 
249
- def self.reset
250
- Circuitbox.reset
230
+ def half_open_storage_key
231
+ @half_open_storage_key ||= "circuits:#{service}:half_open"
251
232
  end
252
-
253
233
  end
254
234
  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
@@ -1,4 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class Circuitbox
2
- class Error < StandardError
3
- end
4
+ class Error < StandardError; end
4
5
  end
@@ -1,10 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class Circuitbox
2
4
  class OpenCircuitError < Circuitbox::Error
3
5
  attr_reader :service
4
6
 
5
7
  def initialize(service)
8
+ super()
6
9
  @service = service
7
10
  end
8
-
9
11
  end
10
12
  end
@@ -1,12 +1,16 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class Circuitbox
2
4
  class ServiceFailureError < Circuitbox::Error
3
5
  attr_reader :service, :original
4
6
 
5
7
  def initialize(service, exception)
8
+ super()
6
9
  @service = service
7
10
  @original = exception
8
11
  # we copy over the original exceptions backtrace if there is one
9
- set_backtrace(exception.backtrace) unless exception.backtrace.empty?
12
+ backtrace = exception.backtrace
13
+ set_backtrace(backtrace) unless backtrace.empty?
10
14
  end
11
15
 
12
16
  def to_s
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'excon'
2
4
  require 'circuitbox'
3
5
 
@@ -8,7 +10,7 @@ class Circuitbox
8
10
  DEFAULT_EXCEPTIONS = [
9
11
  Excon::Errors::Timeout,
10
12
  RequestFailed
11
- ]
13
+ ].freeze
12
14
 
13
15
  class NullResponse < Excon::Response
14
16
  def initialize(response, exception)
@@ -26,36 +28,36 @@ class Circuitbox
26
28
 
27
29
  def initialize(stack, opts = {})
28
30
  @stack = stack
29
- default_options = { open_circuit: lambda { |response| response[:status] >= 400 } }
31
+ default_options = { open_circuit: ->(response) { response[:status] >= 400 } }
30
32
  @opts = default_options.merge(opts)
31
33
  super(stack)
32
34
  end
33
35
 
34
36
  def error_call(datum)
35
- circuit(datum).run!(run_options(datum)) do
37
+ circuit(datum).run do
36
38
  raise RequestFailed
37
39
  end
38
- rescue Circuitbox::Error => exception
39
- circuit_open_value(datum, datum[:response], exception)
40
+ rescue Circuitbox::Error => e
41
+ circuit_open_value(datum, datum[:response], e)
40
42
  end
41
43
 
42
44
  def request_call(datum)
43
- circuit(datum).run!(run_options(datum)) do
45
+ circuit(datum).run do
44
46
  @stack.request_call(datum)
45
47
  end
46
48
  end
47
49
 
48
50
  def response_call(datum)
49
- circuit(datum).run!(run_options(datum)) do
51
+ circuit(datum).run do
50
52
  raise RequestFailed if open_circuit?(datum[:response])
51
53
  end
52
54
  @stack.response_call(datum)
53
- rescue Circuitbox::Error => exception
54
- circuit_open_value(datum, datum[:response], exception)
55
+ rescue Circuitbox::Error => e
56
+ circuit_open_value(datum, datum[:response], e)
55
57
  end
56
58
 
57
59
  def identifier
58
- @identifier ||= opts.fetch(:identifier, ->(env) { env[:path] })
60
+ @identifier ||= opts.fetch(:identifier, ->(env) { env[:host] })
59
61
  end
60
62
 
61
63
  def exceptions
@@ -69,10 +71,6 @@ class Circuitbox
69
71
  circuitbox.circuit id, circuit_breaker_options
70
72
  end
71
73
 
72
- def run_options(datum)
73
- opts.merge(datum)[:circuit_breaker_run_options] || {}
74
- end
75
-
76
74
  def open_circuit?(response)
77
75
  opts[:open_circuit].call(response)
78
76
  end
@@ -86,26 +84,21 @@ class Circuitbox
86
84
  end
87
85
 
88
86
  def circuit_breaker_options
89
- return @circuit_breaker_options if @circuit_breaker_options
90
-
91
- @circuit_breaker_options = opts.fetch(:circuit_breaker_options, {})
92
- @circuit_breaker_options.merge!(
93
- exceptions: opts.fetch(:exceptions, DEFAULT_EXCEPTIONS)
94
- )
87
+ @circuit_breaker_options ||= begin
88
+ options = opts.fetch(:circuit_breaker_options, {})
89
+ options.merge!(
90
+ exceptions: opts.fetch(:exceptions, DEFAULT_EXCEPTIONS)
91
+ )
92
+ end
95
93
  end
96
94
 
97
95
  def default_value
98
- return @default_value if @default_value
99
-
100
- default = opts.fetch(:default_value) do
101
- lambda { |response, exception| NullResponse.new(response, exception) }
96
+ @default_value ||= begin
97
+ default = opts.fetch(:default_value) do
98
+ ->(response, exception) { NullResponse.new(response, exception) }
99
+ end
100
+ default.respond_to?(:call) ? default : ->(*) { default }
102
101
  end
103
-
104
- @default_value = if default.respond_to?(:call)
105
- default
106
- else
107
- lambda { |*| default }
108
- end
109
102
  end
110
103
  end
111
104
  end