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) ⇒ 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



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

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

Instance Attribute Details

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



79
80
81
82
83
84
85
86
# File 'lib/knifeswitch/circuit.rb', line 79

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



90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'lib/knifeswitch/circuit.rb', line 90

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)


69
70
71
72
73
74
75
76
# File 'lib/knifeswitch/circuit.rb', line 69

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



112
113
114
115
116
117
118
# File 'lib/knifeswitch/circuit.rb', line 112

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.



48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# File 'lib/knifeswitch/circuit.rb', line 48

def run
  raise CircuitOpen if open?

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

  raise error
end