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 { ... } ⇒ Result<Object, Error>
-
#unlock! ⇒ self
Remove any open or closed locks.
Constructor Details
#initialize(name, **options) {|Options| ... } ⇒ Circuit
Returns a new instance of Circuit.
185 186 187 188 189 190 191 192 |
# File 'lib/faulty/circuit.rb', line 185 def initialize(name, **, &) raise ArgumentError, 'name must be a String' unless name.is_a?(String) @name = name @given_options = Options.new(, &) @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.
376 377 378 |
# File 'lib/faulty/circuit.rb', line 376 def history storage.history(self) end |
#inspect ⇒ String
Returns Text representation of the circuit.
168 169 170 171 172 173 174 175 176 177 178 179 180 |
# File 'lib/faulty/circuit.rb', line 168 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
333 334 335 336 |
# File 'lib/faulty/circuit.rb', line 333 def lock_closed! storage.lock(self, :closed) self end |
#lock_open! ⇒ self
Force the circuit to stay open until unlocked
325 326 327 328 |
# File 'lib/faulty/circuit.rb', line 325 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.
236 237 238 239 240 241 242 |
# File 'lib/faulty/circuit.rb', line 236 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
351 352 353 354 355 356 |
# File 'lib/faulty/circuit.rb', line 351 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.
308 309 310 311 312 313 314 315 316 317 318 319 320 |
# File 'lib/faulty/circuit.rb', line 308 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 if current_status.can_run? && reserve(current_status) run_exec(current_status, cached_value, cache, &block) else run_skipped(cached_value) end 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.
365 366 367 |
# File 'lib/faulty/circuit.rb', line 365 def status storage.status(self) end |
#try_run { ... } ⇒ Result<Object, Error>
273 274 275 276 277 |
# File 'lib/faulty/circuit.rb', line 273 def try_run(...) Result.new(ok: run(...)) rescue FaultyError => e Result.new(error: e) end |
#unlock! ⇒ self
Remove any open or closed locks
341 342 343 344 |
# File 'lib/faulty/circuit.rb', line 341 def unlock! storage.unlock(self) self end |