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 =
rubocop:disable Metrics/ClassLength
'.faulty_refresh'
Instance Attribute Summary collapse
-
#name ⇒ Object
readonly
Returns the value of attribute name.
-
#options ⇒ Object
readonly
Returns the value of attribute options.
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.
-
#lock_closed! ⇒ self
Force the circuit to stay closed until unlocked.
-
#lock_open! ⇒ self
Force the circuit to stay open until unlocked.
-
#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.
149 150 151 152 153 154 |
# File 'lib/faulty/circuit.rb', line 149 def initialize(name, **, &block) raise ArgumentError, 'name must be a String' unless name.is_a?(String) @name = name @options = Options.new(, &block) 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 |
#options ⇒ Object (readonly)
Returns the value of attribute options.
30 31 32 |
# File 'lib/faulty/circuit.rb', line 30 def @options 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.
281 282 283 |
# File 'lib/faulty/circuit.rb', line 281 def history storage.history(self) end |
#lock_closed! ⇒ self
Force the circuit to stay closed until unlocked
240 241 242 243 |
# File 'lib/faulty/circuit.rb', line 240 def lock_closed! storage.lock(self, :closed) self end |
#lock_open! ⇒ self
Force the circuit to stay open until unlocked
232 233 234 235 |
# File 'lib/faulty/circuit.rb', line 232 def lock_open! storage.lock(self, :open) self end |
#reset! ⇒ self
Reset this circuit to its initial state
This removes the current state, all history, and locks
258 259 260 261 |
# File 'lib/faulty/circuit.rb', line 258 def reset! 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.
218 219 220 221 222 223 224 225 226 227 |
# File 'lib/faulty/circuit.rb', line 218 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.
270 271 272 |
# File 'lib/faulty/circuit.rb', line 270 def status storage.status(self) end |
#try_run(**options) { ... } ⇒ Result<Object, Error>
185 186 187 188 189 |
# File 'lib/faulty/circuit.rb', line 185 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
248 249 250 251 |
# File 'lib/faulty/circuit.rb', line 248 def unlock! storage.unlock(self) self end |