circuitbox 2.0.0.pre4 → 2.0.0.pre5

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fb89d6ef46d73dfa35e7c43181112309aa524979ec1285688cc8cebf8d781bd4
4
- data.tar.gz: 735c23115ea5127923c05b84d5b6cfe64193521bcd751639cc6ceab32c6b97ce
3
+ metadata.gz: 541c5e674d62681f13021254d4304c4903e2812915c59b74fcb419468f608c9d
4
+ data.tar.gz: 97fdbeedc5ac6aa8aa249ca92a1202175e8bbd3f777c4c5211583130aecf5b0d
5
5
  SHA512:
6
- metadata.gz: a29d10725e8a1a36eec12985681cd66b7c2b3303cb996bfe72226674230d7c8ef38b02bb5731665edd49c7922b2a9f0072434a2ab33da7f64713875415071420
7
- data.tar.gz: 9a165096e2b0e5fe484c433284a3c0b6658f4b880552602cb57b22fcf3ea540302b3d3a0910b31b8f3e1938259ca6394faefd3bffffc6c40bf3104d8d02564ec
6
+ metadata.gz: 2466098850251fab23dd6be74a292150ba8eda1e27c3ef1fefcc79aab36dedc170daf4292ae3e9d20ea49468bb487855ce4d3bd77aa7ef1ec8b3ba18bd52d1c6
7
+ data.tar.gz: a62d7c7a283ce4adcec0c73b69db7de8ca982f493369a7ac6534bfc9641de2bc909b8294bb634913b64c1f454861488595e0f103672fa6cc448c7f6beaa8c170
data/README.md CHANGED
@@ -25,7 +25,7 @@ class ExampleServiceClient
25
25
  end
26
26
 
27
27
  def http_get
28
- circuit.run(circuitbox_exceptions: false) do
28
+ circuit.run(exception: false) do
29
29
  Zephyr.new("http://example.com").get(200, 1000, "/api/messages")
30
30
  end
31
31
  end
@@ -43,7 +43,7 @@ Using the `run` method will throw an exception when the circuit is open or the u
43
43
  ```
44
44
 
45
45
  ## Global Configuration
46
- Circuitbox has defaults for circuit_store, notifier, and logger.
46
+ Circuitbox has defaults for circuit_store and notifier.
47
47
  This can be configured through ```Circuitbox.configure```.
48
48
  The circuit cache used by ```Circuitbox.circuit``` will be cleared after running ```Circuitbox.configure```.
49
49
  This means when accessing the circuit through ```Circuitbox.circuit``` any custom configuration options should always be given.
@@ -55,7 +55,6 @@ will need to be recreated to pick up the new defaults.
55
55
  Circuitbox.configure do |config|
56
56
  config.default_circuit_store = Circuitbox::MemoryStore.new
57
57
  config.default_notifier = Circuitbox::Notifier::Null.new
58
- config.default_logger = Rails.logger
59
58
  end
60
59
  ```
61
60
 
@@ -81,15 +80,11 @@ class ExampleServiceClient
81
80
  # the store you want to use to save the circuit state so it can be
82
81
  # tracked, this needs to be Moneta compatible, and support increment
83
82
  # this overrides what is set in the global configuration
84
- cache: Circuitbox::MemoryStore.new,
83
+ circuit_store: Circuitbox::MemoryStore.new,
85
84
 
86
85
  # exceeding this rate will open the circuit (checked on failures)
87
86
  error_threshold: 50,
88
87
 
89
- # Logger to use
90
- # This overrides what is set in the global configuration
91
- logger: Logger.new(STDOUT),
92
-
93
88
  # Customized notifier
94
89
  # overrides the default
95
90
  # this overrides what is set in the global configuration
@@ -108,7 +103,7 @@ Circuitbox.circuit(:yammer, {
108
103
  })
109
104
  ```
110
105
 
111
- ## Circuit Store (:cache)
106
+ ## Circuit Store
112
107
 
113
108
  Holds all the relevant data to trip the circuit if a given number of requests
114
109
  fail in a specified period of time. Circuitbox also supports
@@ -125,60 +120,59 @@ some pre-requisits need to be satisfied first:
125
120
 
126
121
  ## Notifications
127
122
 
128
- circuitbox use ActiveSupport Notifications.
123
+ Circuitbox has two built in notifiers, null and active support.
124
+ The active support notifier is used if `ActiveSupport::Notifications` is defined when circuitbox is loaded.
125
+ If `ActiveSupport::Notifications` is not defined the null notifier is used.
126
+ The null notifier does not send notifications anywhere.
127
+
128
+ The default notifier can be changed to use a specific built in notifier or a custom notifier when [configuring circuitbox](#global-configuration).
129
129
 
130
+ ### ActiveSupport
130
131
  Usage example:
131
132
 
132
- **Log on circuit open/close:**
133
+ **Circuit open/close:**
133
134
 
134
135
  ```ruby
135
- class CircuitOpenException < StandardError ; end
136
-
137
- ActiveSupport::Notifications.subscribe('circuit_open') do |name, start, finish, id, payload|
136
+ ActiveSupport::Notifications.subscribe('open.circuitbox') do |_name, _start, _finish, _id, payload|
138
137
  circuit_name = payload[:circuit]
139
138
  Rails.logger.warn("Open circuit for: #{circuit_name}")
140
139
  end
141
- ActiveSupport::Notifications.subscribe('circuit_close') do |name, start, finish, id, payload|
140
+ ActiveSupport::Notifications.subscribe('close.circuitbox') do |_name, _start, _finish, _id, payload|
142
141
  circuit_name = payload[:circuit]
143
142
  Rails.logger.info("Close circuit for: #{circuit_name}")
144
143
  end
145
144
  ```
146
145
 
147
- **generate metrics:**
146
+ **Circuit run:**
148
147
 
149
148
  ```ruby
150
- $statsd = Statsd.new 'localhost', 9125
151
-
152
- ActiveSupport::Notifications.subscribe('circuit_gauge') do |name, start, finish, id, payload|
153
- circuit_name = payload[:circuit]
154
- gauge = payload[:gauge]
155
- value = payload[:value]
156
- metrics_key = "circuitbox.circuit.#{circuit_name}.#{gauge}"
157
-
158
- $statsd.gauge(metrics_key, value)
149
+ ActiveSupport::Notifications.subscribe('run.circuitbox') do |*args|
150
+ event = ActiveSupport::Notifications::Event.new(*args)
151
+ circuit_name = event.payload[:circuit_name]
152
+
153
+ Rails.logger.info("Circuit: #{circuit_name} Runtime: #{event.duration}")
159
154
  end
160
155
  ```
161
156
 
162
- `payload[:gauge]` can be:
163
-
164
- - `runtime` # runtime will only be notified when circuit is closed and block is successfully executed.
165
-
166
- **warnings:**
167
- in case of misconfiguration, circuitbox will fire a circuitbox_warning
157
+ **Circuit Warnings:**
158
+ In case of misconfiguration, circuitbox will fire a `warning.circuitbox`
168
159
  notification.
169
160
 
170
161
  ```ruby
171
- ActiveSupport::Notifications.subscribe('circuit_warning') do |name, start, finish, id, payload|
162
+ ActiveSupport::Notifications.subscribe('warning.circuitbox') do |_name, _start, _finish, _id, payload|
172
163
  circuit_name = payload[:circuit]
173
164
  warning = payload[:message]
174
- Rails.logger.warning("#{circuit_name} - #{warning}")
165
+ Rails.logger.warning("Circuit warning for: #{circuit_name} Message: #{warning}")
175
166
  end
176
167
 
177
168
  ```
178
169
 
179
170
  ## Faraday
180
171
 
181
- Circuitbox ships with [Faraday HTTP client](https://github.com/lostisland/faraday) middleware.
172
+ Circuitbox ships with a [Faraday HTTP client](https://github.com/lostisland/faraday) middleware.
173
+ The versions of faraday the middleware has been tested against is `>= 0.17` through `~> 2.0`.
174
+ The middleware does not support parallel requests through a connections `in_parallel` method.
175
+
182
176
 
183
177
  ```ruby
184
178
  require 'faraday'
@@ -1,13 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'circuit_breaker/logger_messages'
3
+ require_relative 'time_helper/monotonic'
4
+ require_relative 'time_helper/real'
4
5
 
5
6
  class Circuitbox
6
7
  class CircuitBreaker
7
- include LoggerMessages
8
-
9
8
  attr_reader :service, :circuit_options, :exceptions,
10
- :logger, :circuit_store, :notifier, :time_class
9
+ :circuit_store, :notifier, :time_class
11
10
 
12
11
  DEFAULTS = {
13
12
  sleep_window: 90,
@@ -16,20 +15,23 @@ class Circuitbox
16
15
  time_window: 60
17
16
  }.freeze
18
17
 
18
+ # Initialize a CircuitBreaker
19
19
  #
20
- # Configuration options
21
- #
22
- # `sleep_window` - seconds to sleep the circuit
23
- # `volume_threshold` - number of requests before error rate calculation occurs
24
- # `error_threshold` - percentage of failed requests needed to trip circuit
25
- # `exceptions` - exceptions that count as failures
26
- # `time_window` - interval of time used to calculate error_rate (in seconds) - default is 60s
27
- # `logger` - Logger to use - defaults to Rails.logger if defined, otherwise STDOUT
20
+ # @param service [String, Symbol] Name of the circuit for notifications and metrics store
21
+ # @param options [Hash] Options to create the circuit with
22
+ # @option options [Integer] :time_window (60) Interval of time, in seconds, used to calculate the error_rate
23
+ # @option options [Integer, Proc] :sleep_window (90) Seconds for the circuit to stay open when tripped
24
+ # @option options [Integer, Proc] :volume_threshold (5) Number of requests before error rate is first calculated
25
+ # @option options [Integer, Proc] :error_threshold (50) Percentage of failed requests needed to trip the circuit
26
+ # @option options [Array] :exceptions The exceptions that should be monitored and counted as failures
27
+ # @option options [Circuitbox::MemoryStore, Moneta] :circuit_store (Circuitbox.default_circuit_store) Class to store circuit open/close statistics
28
+ # @option options [Object] :notifier (Circuitbox.default_notifier) Class notifications are sent to
28
29
  #
30
+ # @raise [ArgumentError] If the exceptions option is not an Array
29
31
  def initialize(service, options = {})
30
32
  @service = service.to_s
31
33
  @circuit_options = DEFAULTS.merge(options)
32
- @circuit_store = options.fetch(:cache) { Circuitbox.default_circuit_store }
34
+ @circuit_store = options.fetch(:circuit_store) { Circuitbox.default_circuit_store }
33
35
  @notifier = options.fetch(:notifier) { Circuitbox.default_notifier }
34
36
 
35
37
  if @circuit_options[:timeout_seconds]
@@ -37,47 +39,73 @@ class Circuitbox
37
39
  'Check the upgrade guide at https://github.com/yammer/circuitbox')
38
40
  end
39
41
 
42
+ if @circuit_options[:cache]
43
+ warn('cache was changed to circuit_store in circuitbox 2.0. '\
44
+ 'Check the upgrade guide at https://github.com/yammer/circuitbox')
45
+ end
46
+
40
47
  @exceptions = options.fetch(:exceptions)
41
- raise ArgumentError.new('exceptions need to be an array') unless @exceptions.is_a?(Array)
48
+ raise ArgumentError.new('exceptions must be an array') unless @exceptions.is_a?(Array)
49
+
50
+ @time_class = options.fetch(:time_class) { default_time_klass }
42
51
 
43
- @logger = options.fetch(:logger) { Circuitbox.default_logger }
44
- @time_class = options.fetch(:time_class) { Time }
45
52
  @state_change_mutex = Mutex.new
53
+ @open_storage_key = "circuits:#{@service}:open"
54
+ @half_open_storage_key = "circuits:#{@service}:half_open"
46
55
  check_sleep_window
47
56
  end
48
57
 
49
58
  def option_value(name)
50
- value = circuit_options[name]
59
+ value = @circuit_options[name]
51
60
  value.is_a?(Proc) ? value.call : value
52
61
  end
53
62
 
54
- def run(circuitbox_exceptions: true, &block)
63
+ # Run the circuit with the given block.
64
+ # If the circuit is closed or half_open the block will run.
65
+ # If the circuit is open the block will not be run.
66
+ #
67
+ # @param exception [Boolean] If exceptions should be raised when the circuit is open
68
+ # or when a watched exception is raised from the block
69
+ # @yield Block to run if circuit is not open
70
+ #
71
+ # @raise [Circuitbox::OpenCircuitError] If the circuit is open and exception is true
72
+ # @raise [Circuitbox::ServiceFailureError] If a tracked exception is raised from the block and exception is true
73
+ #
74
+ # @return [Object] The result from the block
75
+ # @return [Nil] If the circuit is open and exception is false
76
+ # In cases where an exception that circuitbox is watching is raised from either a notifier
77
+ # or from a custom circuit store nil can be returned even though the block ran successfully
78
+ def run(exception: true, &block)
55
79
  if open?
56
80
  skipped!
57
- raise Circuitbox::OpenCircuitError.new(service) if circuitbox_exceptions
81
+ raise Circuitbox::OpenCircuitError.new(@service) if exception
58
82
  else
59
- logger.debug(circuit_running_message)
60
-
61
83
  begin
62
- response = Timer.measure(service, notifier, 'runtime', &block)
84
+ response = @notifier.notify_run(@service, &block)
63
85
 
64
86
  success!
65
- rescue *exceptions => e
87
+ rescue *@exceptions => e
66
88
  # 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.
89
+ # setting to nil keeps the same behavior as the previous definition of run.
68
90
  response = nil
69
91
  failure!
70
- raise Circuitbox::ServiceFailureError.new(service, e) if circuitbox_exceptions
92
+ raise Circuitbox::ServiceFailureError.new(@service, e) if exception
71
93
  end
72
94
  end
73
95
 
74
96
  response
75
97
  end
76
98
 
99
+ # Check if the circuit is open
100
+ #
101
+ # @return [Boolean] True if circuit is open, False if closed
77
102
  def open?
78
- circuit_store.key?(open_storage_key)
103
+ @circuit_store.key?(@open_storage_key)
79
104
  end
80
105
 
106
+ # Calculates the current error rate of the circuit
107
+ #
108
+ # @return [Float] Error Rate
81
109
  def error_rate(failures = failure_count, success = success_count)
82
110
  all_count = failures + success
83
111
  return 0.0 unless all_count.positive?
@@ -85,34 +113,52 @@ class Circuitbox
85
113
  (failures / all_count.to_f) * 100
86
114
  end
87
115
 
116
+ # Number of Failures the circuit has encountered in the current time window
117
+ #
118
+ # @return [Integer] Number of failures
88
119
  def failure_count
89
- circuit_store.load(stat_storage_key('failure'), raw: true).to_i
120
+ @circuit_store.load(stat_storage_key('failure'), raw: true).to_i
90
121
  end
91
122
 
123
+ # Number of successes the circuit has encountered in the current time window
124
+ #
125
+ # @return [Integer] Number of successes
92
126
  def success_count
93
- circuit_store.load(stat_storage_key('success'), raw: true).to_i
127
+ @circuit_store.load(stat_storage_key('success'), raw: true).to_i
94
128
  end
95
129
 
130
+ # If the circuit is open the key indicating that the circuit is open
131
+ # On the next call to run the circuit would run as if it were in the half open state
132
+ #
133
+ # This does not reset any of the circuit success/failure state so future failures
134
+ # in the same time window may cause the circuit to open sooner
96
135
  def try_close_next_time
97
- circuit_store.delete(open_storage_key)
136
+ @circuit_store.delete(@open_storage_key)
98
137
  end
99
138
 
100
139
  private
101
140
 
102
141
  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)
142
+ aligned_time = align_time_to_window
143
+
144
+ failures, successes = @circuit_store.values_at(stat_storage_key('failure', aligned_time),
145
+ stat_storage_key('success', aligned_time),
146
+ raw: true)
147
+ # Calling to_i is only needed for moneta stores which can return a string representation of an integer.
148
+ # While readability could increase by adding .map(&:to_i) to the end of the values_at call it's also slightly
149
+ # less performant when we only have two values to convert.
150
+ failures = failures.to_i
151
+ successes = successes.to_i
152
+
153
+ passed_volume_threshold?(failures, successes) && passed_rate_threshold?(failures, successes)
108
154
  end
109
155
 
110
156
  def passed_volume_threshold?(failures, successes)
111
157
  failures + successes >= option_value(:volume_threshold)
112
158
  end
113
159
 
114
- def passed_rate_threshold?(rate)
115
- rate >= option_value(:error_threshold)
160
+ def passed_rate_threshold?(failures, successes)
161
+ error_rate(failures, successes) >= option_value(:error_threshold)
116
162
  end
117
163
 
118
164
  def half_open_failure
@@ -122,7 +168,7 @@ class Circuitbox
122
168
  trip
123
169
  end
124
170
 
125
- # Running event and logger outside of the synchronize block to allow other threads
171
+ # Running event outside of the synchronize block to allow other threads
126
172
  # that may be waiting to become unblocked
127
173
  notify_opened
128
174
  end
@@ -134,19 +180,18 @@ class Circuitbox
134
180
  trip
135
181
  end
136
182
 
137
- # Running event and logger outside of the synchronize block to allow other threads
183
+ # Running event outside of the synchronize block to allow other threads
138
184
  # that may be waiting to become unblocked
139
185
  notify_opened
140
186
  end
141
187
 
142
188
  def notify_opened
143
189
  notify_event('open')
144
- logger.debug(circuit_opened_message)
145
190
  end
146
191
 
147
192
  def trip
148
- circuit_store.store(open_storage_key, true, expires: option_value(:sleep_window))
149
- circuit_store.store(half_open_storage_key, true)
193
+ @circuit_store.store(@open_storage_key, true, expires: option_value(:sleep_window))
194
+ @circuit_store.store(@half_open_storage_key, true)
150
195
  end
151
196
 
152
197
  def close!
@@ -154,29 +199,26 @@ class Circuitbox
154
199
  # If the circuit is not open, the half_open key will be deleted from the store
155
200
  # if half_open exists the deleted value is returned and allows us to continue
156
201
  # 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)
202
+ return unless !open? && @circuit_store.delete(@half_open_storage_key)
158
203
  end
159
204
 
160
205
  # Running event outside of the synchronize block to allow other threads
161
206
  # that may be waiting to become unblocked
162
207
  notify_event('close')
163
- logger.debug(circuit_closed_message)
164
208
  end
165
209
 
166
210
  def half_open?
167
- circuit_store.key?(half_open_storage_key)
211
+ @circuit_store.key?(@half_open_storage_key)
168
212
  end
169
213
 
170
214
  def success!
171
215
  increment_and_notify_event('success')
172
- logger.debug(circuit_success_message)
173
216
 
174
217
  close! if half_open?
175
218
  end
176
219
 
177
220
  def failure!
178
221
  increment_and_notify_event('failure')
179
- logger.debug(circuit_failure_message)
180
222
 
181
223
  if half_open?
182
224
  half_open_failure
@@ -187,20 +229,31 @@ class Circuitbox
187
229
 
188
230
  def skipped!
189
231
  notify_event('skipped')
190
- logger.debug(circuit_skipped_message)
191
232
  end
192
233
 
193
234
  # Send event notification to notifier
194
235
  def notify_event(event)
195
- notifier.notify(service, event)
236
+ @notifier.notify(@service, event)
196
237
  end
197
238
 
198
239
  # Increment stat store and send notification
199
240
  def increment_and_notify_event(event)
200
- circuit_store.increment(stat_storage_key(event), 1, expires: (option_value(:time_window) * 2))
241
+ time_window = option_value(:time_window)
242
+ aligned_time = align_time_to_window(time_window)
243
+ @circuit_store.increment(stat_storage_key(event, aligned_time), 1, expires: time_window)
201
244
  notify_event(event)
202
245
  end
203
246
 
247
+ def stat_storage_key(event, aligned_time = align_time_to_window)
248
+ "circuits:#{@service}:stats:#{aligned_time}:#{event}"
249
+ end
250
+
251
+ # return time representation in seconds
252
+ def align_time_to_window(window = option_value(:time_window))
253
+ time = @time_class.current_second
254
+ time - (time % window) # remove rest of integer division
255
+ end
256
+
204
257
  def check_sleep_window
205
258
  sleep_window = option_value(:sleep_window)
206
259
  time_window = option_value(:time_window)
@@ -208,27 +261,16 @@ class Circuitbox
208
261
 
209
262
  warning_message = "sleep_window: #{sleep_window} is shorter than time_window: #{time_window}, "\
210
263
  "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
214
-
215
- def stat_storage_key(event)
216
- "circuits:#{service}:stats:#{align_time_to_window}:#{event}"
264
+ @notifier.notify_warning(@service, warning_message)
265
+ warn("Circuit: #{@service}, Warning: #{warning_message}")
217
266
  end
218
267
 
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
224
- end
225
-
226
- def open_storage_key
227
- @open_storage_key ||= "circuits:#{service}:open"
228
- end
229
-
230
- def half_open_storage_key
231
- @half_open_storage_key ||= "circuits:#{service}:half_open"
268
+ def default_time_klass
269
+ if @circuit_store.is_a?(Circuitbox::MemoryStore)
270
+ Circuitbox::TimeHelper::Monotonic
271
+ else
272
+ Circuitbox::TimeHelper::Real
273
+ end
232
274
  end
233
275
  end
234
276
  end
@@ -1,16 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'memory_store'
4
- require_relative 'timer'
5
4
  require_relative 'notifier/active_support'
6
5
  require_relative 'notifier/null'
7
6
 
8
7
  class Circuitbox
9
8
  module Configuration
10
9
  attr_writer :default_circuit_store,
11
- :default_notifier,
12
- :default_timer,
13
- :default_logger
10
+ :default_notifier
11
+
12
+ def self.extended(base)
13
+ base.instance_eval do
14
+ @cached_circuits_mutex = Mutex.new
15
+ @cached_circuits = {}
16
+
17
+ # preload circuit_store because it has no other dependencies
18
+ default_circuit_store
19
+ end
20
+ end
14
21
 
15
22
  def configure
16
23
  yield self
@@ -30,22 +37,16 @@ class Circuitbox
30
37
  end
31
38
  end
32
39
 
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
40
  private
42
41
 
43
- def cached_circuits
44
- @cached_circuits ||= {}
42
+ def find_or_create_circuit_breaker(service_name, options)
43
+ @cached_circuits_mutex.synchronize do
44
+ @cached_circuits[service_name] ||= CircuitBreaker.new(service_name, options)
45
+ end
45
46
  end
46
47
 
47
48
  def clear_cached_circuits!
48
- @cached_circuits = {}
49
+ @cached_circuits_mutex.synchronize { @cached_circuits = {} }
49
50
  end
50
51
  end
51
52
  end
@@ -8,5 +8,9 @@ class Circuitbox
8
8
  super()
9
9
  @service = service
10
10
  end
11
+
12
+ def to_s
13
+ "#{self.class}: Service #{service.inspect} has an open circuit"
14
+ end
11
15
  end
12
16
  end
@@ -8,13 +8,13 @@ class Circuitbox
8
8
  super()
9
9
  @service = service
10
10
  @original = exception
11
- # we copy over the original exceptions backtrace if there is one
11
+ # We copy over the original exceptions backtrace if there is one
12
12
  backtrace = exception.backtrace
13
13
  set_backtrace(backtrace) unless backtrace.empty?
14
14
  end
15
15
 
16
16
  def to_s
17
- "#{self.class.name} wrapped: #{original}"
17
+ "#{self.class}: Service #{service.inspect} was unavailable (original: #{original})"
18
18
  end
19
19
  end
20
20
  end
@@ -21,7 +21,7 @@ class Circuitbox
21
21
  open_circuit: lambda do |response|
22
22
  # response.status:
23
23
  # nil -> connection could not be established, or failed very hard
24
- # 5xx -> non recoverable server error, oposed to 4xx which are client errors
24
+ # 5xx -> non recoverable server error, opposed to 4xx which are client errors
25
25
  response.status.nil? || (response.status >= 500 && response.status <= 599)
26
26
  end,
27
27
  default_value: ->(service_response, exception) { NullResponse.new(service_response, exception) },
@@ -34,10 +34,7 @@ class Circuitbox
34
34
  }.freeze
35
35
 
36
36
  DEFAULT_EXCEPTIONS = [
37
- # Faraday before 0.9.0 didn't have Faraday::TimeoutError so we default to Faraday::Error::TimeoutError
38
- # Faraday >= 0.9.0 defines Faraday::TimeoutError and this can be used for all versions up to 1.0.0 that
39
- # also define and raise Faraday::Error::TimeoutError as Faraday::TimeoutError is an ancestor
40
- defined?(Faraday::TimeoutError) ? Faraday::TimeoutError : Faraday::Error::TimeoutError,
37
+ Faraday::TimeoutError,
41
38
  RequestFailed
42
39
  ].freeze
43
40
 
@@ -45,8 +42,6 @@ class Circuitbox
45
42
  exceptions: DEFAULT_EXCEPTIONS
46
43
  }.freeze
47
44
 
48
- attr_reader :opts
49
-
50
45
  def initialize(app, opts = {})
51
46
  @app = app
52
47
  @opts = DEFAULT_OPTIONS.merge(opts)
@@ -70,12 +65,12 @@ class Circuitbox
70
65
  private
71
66
 
72
67
  def call_default_value(response, exception)
73
- default_value = opts[:default_value]
68
+ default_value = @opts[:default_value]
74
69
  default_value.respond_to?(:call) ? default_value.call(response, exception) : default_value
75
70
  end
76
71
 
77
72
  def open_circuit?(response)
78
- opts[:open_circuit].call(response)
73
+ @opts[:open_circuit].call(response)
79
74
  end
80
75
 
81
76
  def circuit_open_value(env, service_response, exception)
@@ -83,12 +78,12 @@ class Circuitbox
83
78
  end
84
79
 
85
80
  def circuit(env)
86
- identifier = opts[:identifier]
81
+ identifier = @opts[:identifier]
87
82
  id = identifier.respond_to?(:call) ? identifier.call(env) : identifier
88
83
 
89
- Circuitbox.circuit(id, opts[:circuit_breaker_options])
84
+ Circuitbox.circuit(id, @opts[:circuit_breaker_options])
90
85
  end
91
86
  end
92
87
  end
93
88
 
94
- Faraday::Middleware.register_middleware circuitbox: Circuitbox::FaradayMiddleware
89
+ Faraday::Middleware.register_middleware(circuitbox: Circuitbox::FaradayMiddleware)
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'monotonic_time'
3
+ require_relative '../time_helper/monotonic'
4
4
 
5
5
  class Circuitbox
6
6
  class MemoryStore
7
7
  class Container
8
- include MonotonicTime
8
+ include TimeHelper::Monotonic
9
9
 
10
10
  attr_accessor :value
11
11
 
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'memory_store/monotonic_time'
3
+ require_relative 'time_helper/monotonic'
4
4
  require_relative 'memory_store/container'
5
5
 
6
6
  class Circuitbox
7
7
  class MemoryStore
8
- include MonotonicTime
8
+ include TimeHelper::Monotonic
9
9
 
10
10
  def initialize(compaction_frequency: 60)
11
11
  @store = {}
@@ -27,7 +27,7 @@ class Circuitbox
27
27
  @mutex.synchronize do
28
28
  existing_container = fetch_container(key)
29
29
 
30
- # reusing the existing container is a small optmization
30
+ # reusing the existing container is a small optimization
31
31
  # to reduce the amount of objects created
32
32
  if existing_container
33
33
  existing_container.expires_after(seconds_to_expire)
@@ -40,7 +40,14 @@ class Circuitbox
40
40
  end
41
41
 
42
42
  def load(key, _opts = {})
43
- @mutex.synchronize { fetch_value(key) }
43
+ @mutex.synchronize { fetch_container(key)&.value }
44
+ end
45
+
46
+ def values_at(*keys, **_opts)
47
+ @mutex.synchronize do
48
+ current_time = current_second
49
+ keys.map! { |key| fetch_container(key, current_time)&.value }
50
+ end
44
51
  end
45
52
 
46
53
  def key?(key)
@@ -53,9 +60,7 @@ class Circuitbox
53
60
 
54
61
  private
55
62
 
56
- def fetch_container(key)
57
- current_time = current_second
58
-
63
+ def fetch_container(key, current_time = current_second)
59
64
  compact(current_time) if @compact_after < current_time
60
65
 
61
66
  container = @store[key]
@@ -70,13 +75,6 @@ class Circuitbox
70
75
  end
71
76
  end
72
77
 
73
- def fetch_value(key)
74
- container = fetch_container(key)
75
- return unless container
76
-
77
- container.value
78
- end
79
-
80
78
  def compact(current_time)
81
79
  @store.delete_if { |_, value| value.expired_at?(current_time) }
82
80
  @compact_after = current_time + @compaction_frequency
@@ -1,18 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Circuitbox
4
- class Notifier
4
+ module Notifier
5
5
  class ActiveSupport
6
6
  def notify(circuit_name, event)
7
- ::ActiveSupport::Notifications.instrument("circuit_#{event}", circuit: circuit_name)
7
+ ::ActiveSupport::Notifications.instrument("#{event}.circuitbox", circuit: circuit_name)
8
8
  end
9
9
 
10
10
  def notify_warning(circuit_name, message)
11
- ::ActiveSupport::Notifications.instrument('circuit_warning', circuit: circuit_name, message: message)
11
+ ::ActiveSupport::Notifications.instrument('warning.circuitbox', circuit: circuit_name, message: message)
12
12
  end
13
13
 
14
- def metric_gauge(circuit_name, gauge, value)
15
- ::ActiveSupport::Notifications.instrument('circuit_gauge', circuit: circuit_name, gauge: gauge, value: value)
14
+ def notify_run(circuit_name, &block)
15
+ ::ActiveSupport::Notifications.instrument('run.circuitbox', circuit: circuit_name, &block)
16
16
  end
17
17
  end
18
18
  end
@@ -1,13 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Circuitbox
4
- class Notifier
4
+ module Notifier
5
5
  class Null
6
- def notify(_, _); end
6
+ def notify(_circuit_name, _event); end
7
7
 
8
- def notify_warning(_, _); end
8
+ def notify_warning(_circuit_name, _message); end
9
9
 
10
- def metric_gauge(_, _, _); end
10
+ def notify_run(_circuit_name)
11
+ yield
12
+ end
11
13
  end
12
14
  end
13
15
  end
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Circuitbox
4
- class MemoryStore
5
- module MonotonicTime
4
+ module TimeHelper
5
+ module Monotonic
6
6
  module_function
7
7
 
8
8
  def current_second
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Circuitbox
4
+ module TimeHelper
5
+ module Real
6
+ module_function
7
+
8
+ def current_second
9
+ ::Time.now.to_i
10
+ end
11
+ end
12
+ end
13
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Circuitbox
4
- VERSION = '2.0.0.pre4'
4
+ VERSION = '2.0.0.pre5'
5
5
  end
data/lib/circuitbox.rb CHANGED
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'logger'
4
-
5
3
  require_relative 'circuitbox/version'
6
4
  require_relative 'circuitbox/circuit_breaker'
7
5
  require_relative 'circuitbox/errors/error'
@@ -10,15 +8,15 @@ require_relative 'circuitbox/errors/service_failure_error'
10
8
  require_relative 'circuitbox/configuration'
11
9
 
12
10
  class Circuitbox
13
- class << self
14
- include Configuration
11
+ extend Configuration
15
12
 
13
+ class << self
16
14
  def circuit(service_name, options, &block)
17
- circuit = (cached_circuits[service_name] ||= CircuitBreaker.new(service_name, options))
15
+ circuit = find_or_create_circuit_breaker(service_name, options)
18
16
 
19
17
  return circuit unless block
20
18
 
21
- circuit.run(circuitbox_exceptions: false, &block)
19
+ circuit.run(exception: false, &block)
22
20
  end
23
21
  end
24
22
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: circuitbox
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0.pre4
4
+ version: 2.0.0.pre5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Fahim Ferdous
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2021-01-21 00:00:00.000000000 Z
12
+ date: 2023-04-06 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bundler
@@ -17,14 +17,14 @@ dependencies:
17
17
  requirements:
18
18
  - - ">"
19
19
  - !ruby/object:Gem::Version
20
- version: '1.16'
20
+ version: '2.0'
21
21
  type: :development
22
22
  prerelease: false
23
23
  version_requirements: !ruby/object:Gem::Requirement
24
24
  requirements:
25
25
  - - ">"
26
26
  - !ruby/object:Gem::Version
27
- version: '1.16'
27
+ version: '2.0'
28
28
  - !ruby/object:Gem::Dependency
29
29
  name: excon
30
30
  requirement: !ruby/object:Gem::Requirement
@@ -45,20 +45,14 @@ dependencies:
45
45
  requirements:
46
46
  - - ">="
47
47
  - !ruby/object:Gem::Version
48
- version: '0.8'
49
- - - "<"
50
- - !ruby/object:Gem::Version
51
- version: '2.0'
48
+ version: '0.17'
52
49
  type: :development
53
50
  prerelease: false
54
51
  version_requirements: !ruby/object:Gem::Requirement
55
52
  requirements:
56
53
  - - ">="
57
54
  - !ruby/object:Gem::Version
58
- version: '0.8'
59
- - - "<"
60
- - !ruby/object:Gem::Version
61
- version: '2.0'
55
+ version: '0.17'
62
56
  - !ruby/object:Gem::Dependency
63
57
  name: gimme
64
58
  requirement: !ruby/object:Gem::Requirement
@@ -79,14 +73,14 @@ dependencies:
79
73
  requirements:
80
74
  - - "~>"
81
75
  - !ruby/object:Gem::Version
82
- version: '5.11'
76
+ version: '5.14'
83
77
  type: :development
84
78
  prerelease: false
85
79
  version_requirements: !ruby/object:Gem::Requirement
86
80
  requirements:
87
81
  - - "~>"
88
82
  - !ruby/object:Gem::Version
89
- version: '5.11'
83
+ version: '5.14'
90
84
  - !ruby/object:Gem::Dependency
91
85
  name: minitest-excludes
92
86
  requirement: !ruby/object:Gem::Requirement
@@ -107,14 +101,14 @@ dependencies:
107
101
  requirements:
108
102
  - - "~>"
109
103
  - !ruby/object:Gem::Version
110
- version: '1.7'
104
+ version: '1.12'
111
105
  type: :development
112
106
  prerelease: false
113
107
  version_requirements: !ruby/object:Gem::Requirement
114
108
  requirements:
115
109
  - - "~>"
116
110
  - !ruby/object:Gem::Version
117
- version: '1.7'
111
+ version: '1.12'
118
112
  - !ruby/object:Gem::Dependency
119
113
  name: moneta
120
114
  requirement: !ruby/object:Gem::Requirement
@@ -158,89 +152,61 @@ dependencies:
158
152
  - !ruby/object:Gem::Version
159
153
  version: '13.0'
160
154
  - !ruby/object:Gem::Dependency
161
- name: rubocop
162
- requirement: !ruby/object:Gem::Requirement
163
- requirements:
164
- - - '='
165
- - !ruby/object:Gem::Version
166
- version: 1.8.1
167
- type: :development
168
- prerelease: false
169
- version_requirements: !ruby/object:Gem::Requirement
170
- requirements:
171
- - - '='
172
- - !ruby/object:Gem::Version
173
- version: 1.8.1
174
- - !ruby/object:Gem::Dependency
175
- name: rubocop-minitest
176
- requirement: !ruby/object:Gem::Requirement
177
- requirements:
178
- - - '='
179
- - !ruby/object:Gem::Version
180
- version: 0.10.3
181
- type: :development
182
- prerelease: false
183
- version_requirements: !ruby/object:Gem::Requirement
184
- requirements:
185
- - - '='
186
- - !ruby/object:Gem::Version
187
- version: 0.10.3
188
- - !ruby/object:Gem::Dependency
189
- name: rubocop-performance
155
+ name: timecop
190
156
  requirement: !ruby/object:Gem::Requirement
191
157
  requirements:
192
- - - '='
158
+ - - "~>"
193
159
  - !ruby/object:Gem::Version
194
- version: 1.9.2
160
+ version: '0.9'
195
161
  type: :development
196
162
  prerelease: false
197
163
  version_requirements: !ruby/object:Gem::Requirement
198
164
  requirements:
199
- - - '='
165
+ - - "~>"
200
166
  - !ruby/object:Gem::Version
201
- version: 1.9.2
167
+ version: '0.9'
202
168
  - !ruby/object:Gem::Dependency
203
- name: rubocop-rake
169
+ name: typhoeus
204
170
  requirement: !ruby/object:Gem::Requirement
205
171
  requirements:
206
- - - '='
172
+ - - "~>"
207
173
  - !ruby/object:Gem::Version
208
- version: 0.5.1
174
+ version: '1.4'
209
175
  type: :development
210
176
  prerelease: false
211
177
  version_requirements: !ruby/object:Gem::Requirement
212
178
  requirements:
213
- - - '='
179
+ - - "~>"
214
180
  - !ruby/object:Gem::Version
215
- version: 0.5.1
181
+ version: '1.4'
216
182
  - !ruby/object:Gem::Dependency
217
- name: timecop
183
+ name: webrick
218
184
  requirement: !ruby/object:Gem::Requirement
219
185
  requirements:
220
186
  - - "~>"
221
187
  - !ruby/object:Gem::Version
222
- version: '0.9'
188
+ version: '1.7'
223
189
  type: :development
224
190
  prerelease: false
225
191
  version_requirements: !ruby/object:Gem::Requirement
226
192
  requirements:
227
193
  - - "~>"
228
194
  - !ruby/object:Gem::Version
229
- version: '0.9'
195
+ version: '1.7'
230
196
  - !ruby/object:Gem::Dependency
231
- name: typhoeus
197
+ name: yard
232
198
  requirement: !ruby/object:Gem::Requirement
233
199
  requirements:
234
200
  - - "~>"
235
201
  - !ruby/object:Gem::Version
236
- version: '1.3'
202
+ version: 0.9.26
237
203
  type: :development
238
204
  prerelease: false
239
205
  version_requirements: !ruby/object:Gem::Requirement
240
206
  requirements:
241
207
  - - "~>"
242
208
  - !ruby/object:Gem::Version
243
- version: '1.3'
209
+ version: 0.9.26
244
210
  description:
245
211
  email:
246
212
  - fahimfmf@gmail.com
@@ -252,7 +218,6 @@ files:
252
218
  - README.md
253
219
  - lib/circuitbox.rb
254
220
  - lib/circuitbox/circuit_breaker.rb
255
- - lib/circuitbox/circuit_breaker/logger_messages.rb
256
221
  - lib/circuitbox/configuration.rb
257
222
  - lib/circuitbox/errors/error.rb
258
223
  - lib/circuitbox/errors/open_circuit_error.rb
@@ -261,10 +226,10 @@ files:
261
226
  - lib/circuitbox/faraday_middleware.rb
262
227
  - lib/circuitbox/memory_store.rb
263
228
  - lib/circuitbox/memory_store/container.rb
264
- - lib/circuitbox/memory_store/monotonic_time.rb
265
229
  - lib/circuitbox/notifier/active_support.rb
266
230
  - lib/circuitbox/notifier/null.rb
267
- - lib/circuitbox/timer.rb
231
+ - lib/circuitbox/time_helper/monotonic.rb
232
+ - lib/circuitbox/time_helper/real.rb
268
233
  - lib/circuitbox/version.rb
269
234
  homepage: https://github.com/yammer/circuitbox
270
235
  licenses:
@@ -273,6 +238,7 @@ metadata:
273
238
  bug_tracker_uri: https://github.com/yammer/circuitbox/issues
274
239
  changelog_uri: https://github.com/yammer/circuitbox/blob/main/CHANGELOG.md
275
240
  source_code_uri: https://github.com/yammer/circuitbox
241
+ rubygems_mfa_required: 'true'
276
242
  post_install_message:
277
243
  rdoc_options: []
278
244
  require_paths:
@@ -281,14 +247,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
281
247
  requirements:
282
248
  - - ">="
283
249
  - !ruby/object:Gem::Version
284
- version: 2.4.0
250
+ version: 2.6.0
285
251
  required_rubygems_version: !ruby/object:Gem::Requirement
286
252
  requirements:
287
253
  - - ">"
288
254
  - !ruby/object:Gem::Version
289
255
  version: 1.3.1
290
256
  requirements: []
291
- rubygems_version: 3.1.4
257
+ rubygems_version: 3.1.6
292
258
  signing_key:
293
259
  specification_version: 4
294
260
  summary: A robust circuit breaker that manages failing external services.
@@ -1,31 +0,0 @@
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
@@ -1,51 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class Circuitbox
4
- class Timer
5
- class Monotonic
6
- class << self
7
- def supported?
8
- defined?(Process::CLOCK_MONOTONIC)
9
- end
10
-
11
- def now
12
- Process.clock_gettime(Process::CLOCK_MONOTONIC)
13
- end
14
- end
15
- end
16
-
17
- class Default
18
- class << self
19
- def supported?
20
- true
21
- end
22
-
23
- def now
24
- Time.now.to_f
25
- end
26
- end
27
- end
28
-
29
- class << self
30
- def measure(service, notifier, metric_name)
31
- before = now
32
- result = yield
33
- total_time = now - before
34
- notifier.metric_gauge(service, metric_name, total_time)
35
- result
36
- end
37
-
38
- private
39
-
40
- if Monotonic.supported?
41
- def now
42
- Monotonic.now
43
- end
44
- else
45
- def now
46
- Default.now
47
- end
48
- end
49
- end
50
- end
51
- end