Class: Faulty::Circuit
- Inherits:
-
Object
- Object
- Faulty::Circuit
- Defined in:
- lib/faulty/circuit.rb
Overview
Runs code protected by a circuit breaker
https://www.martinfowler.com/bliki/CircuitBreaker.html
A circuit is intended to protect against repeated calls to a failing external dependency. For example, a vendor API may be failing continuously. In that case, we trip the circuit breaker and stop calling that API for a specified cool-down period.
Once the cool-down passes, we try the API again, and if it succeeds, we reset the circuit.
Why isn't there a timeout option?
Timeout is inherently unsafe, and should not be used blindly. See Why Ruby's timeout is Dangerous.
You should prefer a network timeout like open_timeout
and read_timeout
, or
write your own code to periodically check how long it has been running.
If you're sure you want ruby's generic Timeout, you can apply it yourself
inside the circuit run block.
Defined Under Namespace
Classes: Options
Constant Summary collapse
- CACHE_REFRESH_SUFFIX =
'.faulty_refresh'
Instance Attribute Summary collapse
-
#name ⇒ Object
readonly
Returns the value of attribute name.
Instance Method Summary collapse
-
#history ⇒ Array<Array>
Get the history of runs of this circuit.
-
#initialize(name, **options) {|Options| ... } ⇒ Circuit
constructor
A new instance of Circuit.
-
#inspect ⇒ String
Text representation of the circuit.
-
#lock_closed! ⇒ self
Force the circuit to stay closed until unlocked.
-
#lock_open! ⇒ self
Force the circuit to stay open until unlocked.
-
#options ⇒ Options
Get the options for this circuit.
-
#reset! ⇒ self
Reset this circuit to its initial state.
-
#run(cache: nil) { ... } ⇒ Object
Run a block protected by this circuit.
-
#status ⇒ Status
Get the current status of the circuit.
- #try_run(**options) { ... } ⇒ Result<Object, Error>
-
#unlock! ⇒ self
Remove any open or closed locks.
Constructor Details
#initialize(name, **options) {|Options| ... } ⇒ Circuit
Returns a new instance of Circuit.
181 182 183 184 185 186 187 188 |
# File 'lib/faulty/circuit.rb', line 181 def initialize(name, **, &block) raise ArgumentError, 'name must be a String' unless name.is_a?(String) @name = name @given_options = Options.new(, &block) @pulled_options = nil @options_pushed = false end |
Instance Attribute Details
#name ⇒ Object (readonly)
Returns the value of attribute name.
29 30 31 |
# File 'lib/faulty/circuit.rb', line 29 def name @name end |
Instance Method Details
#history ⇒ Array<Array>
Get the history of runs of this circuit
The history is an array of tuples where the first value is the run time, and the second value is a boolean which is true if the run was successful.
370 371 372 |
# File 'lib/faulty/circuit.rb', line 370 def history storage.history(self) end |
#inspect ⇒ String
Returns Text representation of the circuit.
164 165 166 167 168 169 170 171 172 173 174 175 176 |
# File 'lib/faulty/circuit.rb', line 164 def inspect interested_opts = %i[ cache_expires_in cache_refreshes_after cache_refresh_jitter cool_down evaluation_window rate_threshold sample_threshold errors exclude ] = .each_pair.map { |k, v| "#{k}: #{v}" if interested_opts.include?(k) }.compact.join(', ') %(#<#{self.class.name} name: #{name}, state: #{status.state}, options: { #{} }>) end |
#lock_closed! ⇒ self
Force the circuit to stay closed until unlocked
327 328 329 330 |
# File 'lib/faulty/circuit.rb', line 327 def lock_closed! storage.lock(self, :closed) self end |
#lock_open! ⇒ self
Force the circuit to stay open until unlocked
319 320 321 322 |
# File 'lib/faulty/circuit.rb', line 319 def lock_open! storage.lock(self, :open) self end |
#options ⇒ Options
Get the options for this circuit
If this circuit has been run, these will the options exactly as given to new. However, if this circuit has not yet been run, these options will be supplemented by the last-known options from the circuit storage.
Once a circuit is run, the given options are pushed to circuit storage to be persisted.
This is to allow circuit objects to behave as expected in contexts where the exact options for a circuit are not known such as an admin dashboard or in a debug console.
Note that this distinction isn't usually important unless using distributed circuit storage like the Redis storage backend.
232 233 234 235 236 237 238 |
# File 'lib/faulty/circuit.rb', line 232 def return @given_options if @options_pushed return @pulled_options if @pulled_options stored = @given_options.storage.(self) @pulled_options = stored ? @given_options.dup_with(stored) : @given_options end |
#reset! ⇒ self
Reset this circuit to its initial state
This removes the current state, all history, and locks
345 346 347 348 349 350 |
# File 'lib/faulty/circuit.rb', line 345 def reset! @options_pushed = false @pulled_options = nil storage.reset(self) self end |
#run(cache: nil) { ... } ⇒ Object
Run a block protected by this circuit
If the circuit is closed, the block will run. Any exceptions raised inside the block will be checked against the error and exclude options to determine whether that error should be captured. If the error is captured, this run will be recorded as a failure.
If the circuit exceeds the failure conditions, this circuit will be tripped and marked as open. Any future calls to run will not execute the block, but instead wait for the cool down period. Once the cool down period passes, the circuit transitions to half-open, and the block will be allowed to run.
If the circuit fails again while half-open, the circuit will be closed for a second cool down period. However, if the circuit completes successfully, the circuit will be closed and reset to its initial state.
When this is run, the given options are persisted to the storage backend.
304 305 306 307 308 309 310 311 312 313 314 |
# File 'lib/faulty/circuit.rb', line 304 def run(cache: nil, &block) cached_value = cache_read(cache) # return cached unless cached.nil? return cached_value if !cached_value.nil? && !cache_should_refresh?(cache) current_status = status return run_skipped(cached_value) unless current_status.can_run? run_exec(current_status, cached_value, cache, &block) end |
#status ⇒ Status
Get the current status of the circuit
This method is not safe for concurrent operations, so it's unsafe to check this method and make runtime decisions based on that. However, it's useful for getting a non-synchronized snapshot of a circuit.
359 360 361 |
# File 'lib/faulty/circuit.rb', line 359 def status storage.status(self) end |
#try_run(**options) { ... } ⇒ Result<Object, Error>
269 270 271 272 273 |
# File 'lib/faulty/circuit.rb', line 269 def try_run(**, &block) Result.new(ok: run(**, &block)) rescue FaultyError => e Result.new(error: e) end |
#unlock! ⇒ self
Remove any open or closed locks
335 336 337 338 |
# File 'lib/faulty/circuit.rb', line 335 def unlock! storage.unlock(self) self end |