Class: Knifeswitch::Circuit

Inherits:
Object
  • Object
show all
Defined in:
lib/knifeswitch/circuit.rb

Overview

Implements the “circuit breaker” pattern using a simple MySQL table.

Example usage:

circuit = Knifeswitch::Circuit.new(
  namespace:       'some third-party',
  exceptions:      [Example::TimeoutError],
  error_threshold: 5,
  error_timeout:   30
)
response = circuit.run { client.request(...) }

In this example, when a TimeoutError is raised within a circuit.run block 5 times in a row, the circuit will “open” and further calls to circuit.run will raise Knifeswitch::CircuitOpen instead of executing the block. After 30 seconds, the circuit “closes” and circuit.run blocks will be run again.

Two circuits with the same namespace share the same counter and open/closed state, as long as they’re connected to the same database.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(namespace: 'default', exceptions: [Timeout::Error], error_threshold: 10, error_timeout: 60, callback: nil) ⇒ Circuit

Options:

namespace: circuits in the same namespace share state exceptions: an array of error types that bump the counter error_threshold: number of errors required to open the circuit error_timeout: seconds to keep the circuit open



33
34
35
36
37
38
39
40
41
42
43
44
45
# File 'lib/knifeswitch/circuit.rb', line 33

def initialize(
  namespace: 'default',
  exceptions: [Timeout::Error],
  error_threshold: 10,
  error_timeout: 60,
  callback: nil
)
  @namespace       = namespace
  @exceptions      = exceptions
  @error_threshold = error_threshold
  @error_timeout   = error_timeout
  @callback        = callback
end

Instance Attribute Details

#callbackObject

Returns the value of attribute callback.



25
26
27
# File 'lib/knifeswitch/circuit.rb', line 25

def callback
  @callback
end

#error_thresholdObject (readonly)

Returns the value of attribute error_threshold.



24
25
26
# File 'lib/knifeswitch/circuit.rb', line 24

def error_threshold
  @error_threshold
end

#error_timeoutObject (readonly)

Returns the value of attribute error_timeout.



24
25
26
# File 'lib/knifeswitch/circuit.rb', line 24

def error_timeout
  @error_timeout
end

#exceptionsObject (readonly)

Returns the value of attribute exceptions.



24
25
26
# File 'lib/knifeswitch/circuit.rb', line 24

def exceptions
  @exceptions
end

#namespaceObject (readonly)

Returns the value of attribute namespace.



24
25
26
# File 'lib/knifeswitch/circuit.rb', line 24

def namespace
  @namespace
end

Instance Method Details

#counterObject

Retrieves the current counter value.



86
87
88
89
90
91
92
93
# File 'lib/knifeswitch/circuit.rb', line 86

def counter
  result = sql(:select_value, %(
    SELECT counter FROM knifeswitch_counters
    WHERE name = ?
  ), namespace)

  result || 0
end

#increment_counter!Object

Increments counter and opens the circuit if it went too high



97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
# File 'lib/knifeswitch/circuit.rb', line 97

def increment_counter!
  # Increment the counter
  sql(:execute, %(
    INSERT INTO knifeswitch_counters (name,counter)
    VALUES (?, 1)
    ON DUPLICATE KEY UPDATE counter=counter+1
  ), namespace)

  # Possibly open the circuit
  sql(
    :execute,
    %(
      UPDATE knifeswitch_counters
      SET closetime = ?
      WHERE name = ? AND COUNTER >= ?
    ),
    DateTime.now + error_timeout.seconds,
    namespace, error_threshold
  )
end

#open?Boolean

Queries the database to see if the circuit is open.

The circuit opens when ‘error_threshold’ errors occur consecutively. When the circuit is open, calls to ‘run` will raise CircuitOpen instead of yielding.

Returns:

  • (Boolean)


76
77
78
79
80
81
82
83
# File 'lib/knifeswitch/circuit.rb', line 76

def open?
  result = sql(:select_value, %(
    SELECT COUNT(*) c FROM knifeswitch_counters
    WHERE name = ? AND closetime > ?
  ), namespace, DateTime.now)

  result > 0
end

#reset_counter!Object

Sets the counter to zero



119
120
121
122
123
124
125
# File 'lib/knifeswitch/circuit.rb', line 119

def reset_counter!
  sql(:execute, %(
    INSERT INTO knifeswitch_counters (name,counter)
    VALUES (?, 0)
    ON DUPLICATE KEY UPDATE counter=0
  ), namespace)
end

#runObject

Call this with a block to execute the contents of the block under circuit breaker protection.

Raises Knifeswitch::CircuitOpen when called while the circuit is open.



51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# File 'lib/knifeswitch/circuit.rb', line 51

def run
  if open?
    callback.try(:call, CircuitOpen.new)
    raise CircuitOpen
  end

  result = yield
  reset_counter!
  result
rescue Exception => error
  if exceptions.any? { |watched| error.is_a?(watched) }
    increment_counter!
    callback.try(:call, error)
  else
    reset_counter!
  end

  raise error
end