Class: Circuitbox::CircuitBreaker

Inherits:
Object
  • Object
show all
Defined in:
lib/circuitbox/circuit_breaker.rb

Constant Summary collapse

DEFAULTS =
{
  sleep_window: 90,
  volume_threshold: 5,
  error_threshold: 50,
  time_window: 60
}.freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(service, options = {}) ⇒ CircuitBreaker

Initialize a CircuitBreaker

Parameters:

  • service (String, Symbol)

    Name of the circuit for notifications and metrics store

  • options (Hash) (defaults to: {})

    Options to create the circuit with

Options Hash (options):

  • :time_window (Integer) — default: 60

    Interval of time, in seconds, used to calculate the error_rate

  • :sleep_window (Integer, Proc) — default: 90

    Seconds for the circuit to stay open when tripped

  • :volume_threshold (Integer, Proc) — default: 5

    Number of requests before error rate is first calculated

  • :error_threshold (Integer, Proc) — default: 50

    Percentage of failed requests needed to trip the circuit

  • :exceptions (Array)

    The exceptions that should be monitored and counted as failures

  • :circuit_store (Circuitbox::MemoryStore, Moneta) — default: Circuitbox.default_circuit_store

    Class to store circuit open/close statistics

  • :notifier (Object) — default: Circuitbox.default_notifier

    Class notifications are sent to

Raises:

  • (ArgumentError)

    If the exceptions option is not an Array



31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# File 'lib/circuitbox/circuit_breaker.rb', line 31

def initialize(service, options = {})
  @service = service.to_s
  @circuit_options = DEFAULTS.merge(options)
  @circuit_store   = options.fetch(:circuit_store) { Circuitbox.default_circuit_store }
  @notifier = options.fetch(:notifier) { Circuitbox.default_notifier }

  if @circuit_options[:timeout_seconds]
    warn('timeout_seconds was removed in circuitbox 2.0. '\
         'Check the upgrade guide at https://github.com/yammer/circuitbox')
  end

  if @circuit_options[:cache]
    warn('cache was changed to circuit_store in circuitbox 2.0. '\
         'Check the upgrade guide at https://github.com/yammer/circuitbox')
  end

  @exceptions = options.fetch(:exceptions)
  raise ArgumentError.new('exceptions must be an array') unless @exceptions.is_a?(Array)

  @time_class = options.fetch(:time_class) { default_time_klass }

  @state_change_mutex = Mutex.new
  @open_storage_key = "circuits:#{@service}:open"
  @half_open_storage_key = "circuits:#{@service}:half_open"
  check_sleep_window
end

Instance Attribute Details

#circuit_optionsObject (readonly)

Returns the value of attribute circuit_options.



8
9
10
# File 'lib/circuitbox/circuit_breaker.rb', line 8

def circuit_options
  @circuit_options
end

#circuit_storeObject (readonly)

Returns the value of attribute circuit_store.



8
9
10
# File 'lib/circuitbox/circuit_breaker.rb', line 8

def circuit_store
  @circuit_store
end

#exceptionsObject (readonly)

Returns the value of attribute exceptions.



8
9
10
# File 'lib/circuitbox/circuit_breaker.rb', line 8

def exceptions
  @exceptions
end

#notifierObject (readonly)

Returns the value of attribute notifier.



8
9
10
# File 'lib/circuitbox/circuit_breaker.rb', line 8

def notifier
  @notifier
end

#serviceObject (readonly)

Returns the value of attribute service.



8
9
10
# File 'lib/circuitbox/circuit_breaker.rb', line 8

def service
  @service
end

#time_classObject (readonly)

Returns the value of attribute time_class.



8
9
10
# File 'lib/circuitbox/circuit_breaker.rb', line 8

def time_class
  @time_class
end

Instance Method Details

#error_rate(failures = failure_count, success = success_count) ⇒ Float

Calculates the current error rate of the circuit

Returns:

  • (Float)

    Error Rate



109
110
111
112
113
114
# File 'lib/circuitbox/circuit_breaker.rb', line 109

def error_rate(failures = failure_count, success = success_count)
  all_count = failures + success
  return 0.0 unless all_count.positive?

  (failures / all_count.to_f) * 100
end

#failure_countInteger

Number of Failures the circuit has encountered in the current time window

Returns:

  • (Integer)

    Number of failures



119
120
121
# File 'lib/circuitbox/circuit_breaker.rb', line 119

def failure_count
  @circuit_store.load(stat_storage_key('failure'), raw: true).to_i
end

#open?Boolean

Check if the circuit is open

Returns:

  • (Boolean)

    True if circuit is open, False if closed



102
103
104
# File 'lib/circuitbox/circuit_breaker.rb', line 102

def open?
  @circuit_store.key?(@open_storage_key)
end

#option_value(name) ⇒ Object



58
59
60
61
# File 'lib/circuitbox/circuit_breaker.rb', line 58

def option_value(name)
  value = @circuit_options[name]
  value.is_a?(Proc) ? value.call : value
end

#run(exception: true) { ... } ⇒ Object, Nil

Run the circuit with the given block. If the circuit is closed or half_open the block will run. If the circuit is open the block will not be run.

Parameters:

  • exception (Boolean) (defaults to: true)

    If exceptions should be raised when the circuit is open or when a watched exception is raised from the block

Yields:

  • Block to run if circuit is not open

Returns:

  • (Object)

    The result from the block

  • (Nil)

    If the circuit is open and exception is false In cases where an exception that circuitbox is watching is raised from either a notifier or from a custom circuit store nil can be returned even though the block ran successfully

Raises:



78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/circuitbox/circuit_breaker.rb', line 78

def run(exception: true, &block)
  if open?
    skipped!
    raise Circuitbox::OpenCircuitError.new(@service) if exception
  else
    begin
      response = @notifier.notify_run(@service, &block)

      success!
    rescue *@exceptions => e
      # Other stores could raise an exception that circuitbox is asked to watch.
      # setting to nil keeps the same behavior as the previous definition of run.
      response = nil
      failure!
      raise Circuitbox::ServiceFailureError.new(@service, e) if exception
    end
  end

  response
end

#success_countInteger

Number of successes the circuit has encountered in the current time window

Returns:

  • (Integer)

    Number of successes



126
127
128
# File 'lib/circuitbox/circuit_breaker.rb', line 126

def success_count
  @circuit_store.load(stat_storage_key('success'), raw: true).to_i
end

#try_close_next_timeObject

If the circuit is open the key indicating that the circuit is open On the next call to run the circuit would run as if it were in the half open state

This does not reset any of the circuit success/failure state so future failures in the same time window may cause the circuit to open sooner



135
136
137
# File 'lib/circuitbox/circuit_breaker.rb', line 135

def try_close_next_time
  @circuit_store.delete(@open_storage_key)
end