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 =

rubocop:disable Metrics/ClassLength

'.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)


138
139
140
141
142
143
# File 'lib/faulty/circuit.rb', line 138

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]



268
269
270
# File 'lib/faulty/circuit.rb', line 268

def history
  storage.history(self)
end

#lock_closed!self

Force the circuit to stay closed until unlocked

Returns:

  • (self)


227
228
229
230
# File 'lib/faulty/circuit.rb', line 227

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

#lock_open!self

Force the circuit to stay open until unlocked

Returns:

  • (self)


219
220
221
222
# File 'lib/faulty/circuit.rb', line 219

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)


245
246
247
248
# File 'lib/faulty/circuit.rb', line 245

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



207
208
209
210
211
212
213
214
# File 'lib/faulty/circuit.rb', line 207

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:



257
258
259
# File 'lib/faulty/circuit.rb', line 257

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.



174
175
176
177
178
# File 'lib/faulty/circuit.rb', line 174

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)


235
236
237
238
# File 'lib/faulty/circuit.rb', line 235

def unlock!
  storage.unlock(self)
  self
end