Class: Knifeswitch::Circuit
- Inherits:
-
Object
- Object
- Knifeswitch::Circuit
- 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
-
#callback ⇒ Object
Returns the value of attribute callback.
-
#error_threshold ⇒ Object
readonly
Returns the value of attribute error_threshold.
-
#error_timeout ⇒ Object
readonly
Returns the value of attribute error_timeout.
-
#exceptions ⇒ Object
readonly
Returns the value of attribute exceptions.
-
#namespace ⇒ Object
readonly
Returns the value of attribute namespace.
Instance Method Summary collapse
-
#counter ⇒ Object
Retrieves the current counter value.
-
#increment_counter! ⇒ Object
Increments counter and opens the circuit if it went too high.
-
#initialize(namespace: 'default', exceptions: [Timeout::Error], error_threshold: 10, error_timeout: 60, callback: nil) ⇒ Circuit
constructor
Options:.
-
#open? ⇒ Boolean
Queries the database to see if the circuit is open.
-
#reset_counter! ⇒ Object
Sets the counter to zero.
-
#run ⇒ Object
Call this with a block to execute the contents of the block under circuit breaker protection.
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
#callback ⇒ Object
Returns the value of attribute callback.
25 26 27 |
# File 'lib/knifeswitch/circuit.rb', line 25 def callback @callback end |
#error_threshold ⇒ Object (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_timeout ⇒ Object (readonly)
Returns the value of attribute error_timeout.
24 25 26 |
# File 'lib/knifeswitch/circuit.rb', line 24 def error_timeout @error_timeout end |
#exceptions ⇒ Object (readonly)
Returns the value of attribute exceptions.
24 25 26 |
# File 'lib/knifeswitch/circuit.rb', line 24 def exceptions @exceptions end |
#namespace ⇒ Object (readonly)
Returns the value of attribute namespace.
24 25 26 |
# File 'lib/knifeswitch/circuit.rb', line 24 def namespace @namespace end |
Instance Method Details
#counter ⇒ Object
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.
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 |
#run ⇒ Object
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 |