Class: Faulty::Circuit

Inherits:
Object
  • Object
show all
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

Instance Method Summary collapse

Constructor Details

#initialize(name, **options) {|Options| ... } ⇒ Circuit

Returns a new instance of Circuit.

Parameters:

  • name (String)

    The name of the circuit

  • options (Hash)

    Attributes for Options

Yields:

  • (Options)

    For setting options in a block

Raises:

  • (ArgumentError)

143
144
145
146
147
148
# File 'lib/faulty/circuit.rb', line 143

def initialize(name, **options, &block)
  raise ArgumentError, 'name must be a String' unless name.is_a?(String)

  @name = name
  @options = Options.new(options, &block)
end

Instance Attribute Details

#nameObject (readonly)

Returns the value of attribute name


29
30
31
# File 'lib/faulty/circuit.rb', line 29

def name
  @name
end

#optionsObject (readonly)

Returns the value of attribute options


30
31
32
# File 'lib/faulty/circuit.rb', line 30

def options
  @options
end

Instance Method Details

#historyArray<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.

Returns:

  • (Array<Array>)

    ] An array of tuples of [run_time, is_success]


273
274
275
# File 'lib/faulty/circuit.rb', line 273

def history
  storage.history(self)
end

#lock_closed!self

Force the circuit to stay closed until unlocked

Returns:

  • (self)

232
233
234
235
# File 'lib/faulty/circuit.rb', line 232

def lock_closed!
  storage.lock(self, :closed)
  self
end

#lock_open!self

Force the circuit to stay open until unlocked

Returns:

  • (self)

224
225
226
227
# File 'lib/faulty/circuit.rb', line 224

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

Returns:

  • (self)

250
251
252
253
# File 'lib/faulty/circuit.rb', line 250

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.

Parameters:

  • cache (String, nil) (defaults to: nil)

    A cache key, or nil if caching is not desired

Yields:

  • The block to protect with this circuit

Returns:

  • The return value of the block

Raises:

  • If the block raises an error not in the error list, or if the error is excluded.

  • (OpenCircuitError)

    if the circuit is open

  • (CircuitTrippedError)

    if this run causes the circuit to trip. It's possible for concurrent runs to simultaneously trip the circuit if the storage engine is not concurrency-safe.

  • (CircuitFailureError)

    if this run fails, but doesn't cause the circuit to trip


212
213
214
215
216
217
218
219
# File 'lib/faulty/circuit.rb', line 212

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)
  return run_skipped(cached_value) unless status.can_run?

  run_exec(cached_value, cache, &block)
end

#statusStatus

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.

Returns:


262
263
264
# File 'lib/faulty/circuit.rb', line 262

def status
  storage.status(self)
end

#try_run(**options) { ... } ⇒ Result<Object, Error>

Run the circuit as with #run, but return a Result

This is syntax sugar for running a circuit and rescuing an error

Examples:

result = Faulty.circuit(:api).try_run do
  api.get
end

response = if result.ok?
  result.get
else
  { error: result.error.message }
end
# The Result object has a fetch method that can return a default value
# if an error occurs
result = Faulty.circuit(:api).try_run do
  api.get
end.fetch({})

Parameters:

  • cache (String, nil)

    A cache key, or nil if caching is not desired

Yields:

  • The block to protect with this circuit

Returns:

  • (Result<Object, Error>)

    A result where the ok value is the return value of the block, or the error value is an error captured by the circuit.

Raises:

  • If the block raises an error not in the error list, or if the error is excluded.


179
180
181
182
183
# File 'lib/faulty/circuit.rb', line 179

def try_run(**options, &block)
  Result.new(ok: run(**options, &block))
rescue FaultyError => e
  Result.new(error: e)
end

#unlock!self

Remove any open or closed locks

Returns:

  • (self)

240
241
242
243
# File 'lib/faulty/circuit.rb', line 240

def unlock!
  storage.unlock(self)
  self
end