Class: AllMyCircuits::Breaker

Inherits:
Object
  • Object
show all
Includes:
Logging
Defined in:
lib/all_my_circuits/breaker.rb

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Logging

#debug, #info

Constructor Details

#initialize(name:, watch_errors: Breaker.net_errors, sleep_seconds:, strategy:, notifier: Notifiers::NullNotifier.new, clock: Clock) ⇒ Breaker

Public: Initializes circuit breaker instance.

Options

name          - name of the call wrapped into circuit breaker (e.g. "That Unstable Service").
watch_errors  - exceptions to count as failures. Other exceptions will simply get re-raised
                (default: AllMyCircuits::Breaker.net_errors).
sleep_seconds - number of seconds the circuit stays open before attempting to close.
strategy      - an AllMyCircuits::Strategies::AbstractStrategy-compliant object that controls
                when the circuit should be tripped open.
                Built-in strategies:
                  AllMyCircuits::Strategies::PercentageOverWindowStrategy,
                  AllMyCircuits::Strategies::NumberOverWindowStrategy.
notifier      - (optional) AllMyCircuits::Notifiers::AbstractNotifier-compliant object that
                is called whenever circuit breaker state (open, closed) changes.
                Built-in notifiers:
                  AllMyCircuits::Notifiers::NullNotifier.

Examples

AllMyCircuits::Breaker.new(
  name: "My Unstable Service",
  sleep_seconds: 5,
  strategy: AllMyCircuits::Strategies::PercentageOverWindowStrategy.new(
    requests_window: 20,                 # number of requests in the window to calculate failure rate for
    failure_rate_percent_threshold: 25   # how many failures can occur within the window, in percent,
  )                                      #   before the circuit opens
)

AllMyCircuits::Breaker.new(
  name: "Another Unstable Service",
  sleep_seconds: 5,
  strategy: AllMyCircuits::Strategies::NumberOverWindowStrategy.new(
    requests_window: 20,
    failures_threshold: 25         # how many failures can occur within the window before the circuit opens
  )
)


59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# File 'lib/all_my_circuits/breaker.rb', line 59

def initialize(name:,
               watch_errors: Breaker.net_errors,
               sleep_seconds:,
               strategy:,
               notifier: Notifiers::NullNotifier.new,
               clock: Clock)

  @name = String(name).dup.freeze
  @watch_errors = Array(watch_errors).dup
  @sleep_seconds = Integer(sleep_seconds)

  @strategy = strategy
  @notifier = notifier

  @state_lock = Mutex.new
  @request_number = Concurrent::AtomicReference.new(0)
  @last_open_or_probed = nil
  @opened_at_request_number = 0
  @clock = clock
end

Instance Attribute Details

#nameObject (readonly)

Returns the value of attribute name.



9
10
11
# File 'lib/all_my_circuits/breaker.rb', line 9

def name
  @name
end

Class Method Details

.net_errorsObject

Public: exceptions typically thrown when using Net::HTTP



13
14
15
16
17
18
19
# File 'lib/all_my_circuits/breaker.rb', line 13

def self.net_errors
  require "timeout"
  require "net/http"

  [Timeout::Error, Errno::ECONNRESET, Errno::ECONNREFUSED, EOFError,
   Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::HTTPFatalError, Net::HTTPServerError]
end

Instance Method Details

#runObject

Public: executes supplied block of code and monitors failures. Once the number of failures reaches a certain threshold, the block is bypassed for a certain period of time.

Consider the following examples of calls through circuit breaker (let it be 1 call per second, and let the circuit breaker be configured as in the example below):

Legend

S - successful request
F - failed request
O - skipped request (circuit open)
| - open circuit interval end

1) S S F F S S F F S F O O O O O|S S S S S

Here among the first 10 requests (window), 5 failures occur (50%), the circuit is tripped open
for 5 seconds, and a few requests are skipped. Then, after 5 seconds, a request is issued to
see whether everything is back to normal, and the circuit is then closed again.

2) S S F F S S F F S F O O O O O|F O O O O O|S S S S S

Same situation, 10 requests, 5 failed, circuit is tripped open for 5 seconds. Then we
check that service is back to normal, and it is not. The circuit is open again.
After another 5 seconds we check again and close the circuit.

Returns nothing. Raises AllMyCircuit::BreakerOpen with the name of the service when the circuit is open. Raises whatever has been raised by the supplied block.

This call is thread-safe sans the supplied block.

Examples

@cb = AllMyCircuits::Breaker.new(
  name: "that bad service",
  sleep_seconds: 5,
  strategy: AllMyCircuits::Strategies::PercentageOverWindowStrategy.new(
    requests_window: 10,
    failure_rate_percent_threshold: 50
  )
)

@client = MyBadServiceClient.new(timeout: 2)

begin
  @cb.run do
    @client.make_expensive_unreliable_http_call
  end
rescue AllMyCircuits::BreakerOpen => e
  []
rescue MyBadServiceClient::Error => e
  MyLog << "an error has occured in call to my bad service"
  []
end


137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
# File 'lib/all_my_circuits/breaker.rb', line 137

def run
  unless allow_request?
    debug "declining request, circuit is open", name
    raise BreakerOpen, @name
  end

  current_request_number = generate_request_number
  begin
    result = yield
    success(current_request_number)
    result
  rescue *@watch_errors
    error(current_request_number)
    raise
  end
end