circuitbox 2.0.0.pre4 → 2.0.0.pre5

Sign up to get free protection for your applications and to get access to all the features.
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