supervision 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +12 -0
- data/README.md +20 -10
- data/lib/supervision.rb +30 -2
- data/lib/supervision/circuit_breaker.rb +85 -21
- data/lib/supervision/circuit_control.rb +61 -19
- data/lib/supervision/circuit_monitor.rb +47 -2
- data/lib/supervision/circuit_system.rb +31 -2
- data/lib/supervision/configuration.rb +15 -2
- data/lib/supervision/counter.rb +49 -0
- data/lib/supervision/registry.rb +37 -3
- data/lib/supervision/version.rb +1 -1
- data/spec/spec_helper.rb +25 -0
- data/spec/unit/circuit_breaker_spec.rb +37 -12
- data/spec/unit/circuit_control_spec.rb +49 -18
- data/spec/unit/circuit_monitor_spec.rb +21 -0
- data/spec/unit/circuit_system_spec.rb +42 -0
- data/spec/unit/configuration_spec.rb +29 -3
- data/spec/unit/counter_spec.rb +34 -0
- data/spec/unit/initialize_spec.rb +68 -11
- data/spec/unit/registry_spec.rb +38 -7
- data/supervision.gemspec +1 -1
- data/tasks/console.rake +1 -1
- metadata +10 -4
@@ -4,10 +4,55 @@ module Supervision
|
|
4
4
|
# A class responsible for recording circuit performance
|
5
5
|
class CircuitMonitor
|
6
6
|
|
7
|
+
attr_reader :times_opened
|
8
|
+
|
9
|
+
# Timestamp for the last circuit open state
|
10
|
+
#
|
11
|
+
# @api public
|
12
|
+
attr_reader :last_opened
|
13
|
+
|
7
14
|
def initialize
|
15
|
+
@total_failed_calls = Counter.new
|
16
|
+
@total_success_calls = Counter.new
|
17
|
+
@total_calls = Counter.new
|
18
|
+
@state_transitions = Counter.new
|
19
|
+
end
|
20
|
+
|
21
|
+
def total_calls
|
22
|
+
@total_calls.value
|
23
|
+
end
|
24
|
+
|
25
|
+
def total_success_calls
|
26
|
+
@total_success_calls.value
|
27
|
+
end
|
28
|
+
|
29
|
+
def total_failed_calls
|
30
|
+
@total_failed_calls.value
|
31
|
+
end
|
32
|
+
|
33
|
+
def record_success
|
34
|
+
@total_success_calls.increment
|
35
|
+
@total_calls.increment
|
36
|
+
end
|
37
|
+
|
38
|
+
def record_failure
|
39
|
+
@total_failed_calls.increment
|
40
|
+
@total_calls.increment
|
41
|
+
end
|
42
|
+
|
43
|
+
def measure(type)
|
44
|
+
|
8
45
|
end
|
9
46
|
|
10
|
-
|
47
|
+
# Reset the circuit statistics
|
48
|
+
#
|
49
|
+
# @return [nil]
|
50
|
+
#
|
51
|
+
# @api public
|
52
|
+
def reset
|
53
|
+
total_calls.clear
|
54
|
+
total_success_calls.clear
|
55
|
+
total_failed_calls.clear
|
11
56
|
end
|
12
|
-
end
|
57
|
+
end # CircuitMonitor
|
13
58
|
end # Supervision
|
@@ -5,12 +5,41 @@ module Supervision
|
|
5
5
|
class CircuitSystem
|
6
6
|
extend Forwardable
|
7
7
|
|
8
|
-
|
9
|
-
:register, :delete, :unregister
|
8
|
+
attr_reader :registry
|
10
9
|
|
10
|
+
def_delegators '@registry', :[], :get, :[]=, :set, :register, :delete,
|
11
|
+
:unregister, :names, :empty?, :registered?
|
12
|
+
|
13
|
+
# Create a CircuitSystem
|
14
|
+
#
|
15
|
+
# @api public
|
11
16
|
def initialize
|
12
17
|
@registry = Registry.new
|
13
18
|
end
|
14
19
|
|
20
|
+
# Shutdown this circuit system
|
21
|
+
#
|
22
|
+
# @api public
|
23
|
+
def shutdown
|
24
|
+
@registry.clear
|
25
|
+
end
|
26
|
+
|
27
|
+
# Detailed string representation of this circuit system
|
28
|
+
#
|
29
|
+
# @return [String]
|
30
|
+
#
|
31
|
+
# @api public
|
32
|
+
def inspect
|
33
|
+
"#<#{self.class.name}:#{object_id}> @names=#{names}>"
|
34
|
+
end
|
35
|
+
|
36
|
+
# Detailed string representation of this circuit system
|
37
|
+
#
|
38
|
+
# @return [String]
|
39
|
+
#
|
40
|
+
# @api public
|
41
|
+
def to_s
|
42
|
+
"#<#{self.class.name}:#{object_id}> @names=#{names}>"
|
43
|
+
end
|
15
44
|
end # CircuitSystem
|
16
45
|
end # Supervision
|
@@ -22,6 +22,19 @@ module Supervision
|
|
22
22
|
DEFAULT_RESET_TIMEOUT))
|
23
23
|
end
|
24
24
|
|
25
|
+
# Evalutate this configuration
|
26
|
+
#
|
27
|
+
# @return [self]
|
28
|
+
#
|
29
|
+
# @api public
|
30
|
+
def configure(&block)
|
31
|
+
if block.arity.zero?
|
32
|
+
instance_eval(&block)
|
33
|
+
else
|
34
|
+
yield self
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
25
38
|
def max_failures=(value)
|
26
39
|
@max_failures.set(value)
|
27
40
|
end
|
@@ -64,9 +77,9 @@ module Supervision
|
|
64
77
|
end
|
65
78
|
end
|
66
79
|
|
67
|
-
# TODO: replace with custom error
|
68
80
|
def raise_unknown_config_option(option)
|
69
|
-
raise
|
81
|
+
raise InvalidParameterError,
|
82
|
+
"`#{option}` isn`t recognized as valid parameter." \
|
70
83
|
" Please use one of `#{known_options.join(', ')}`"
|
71
84
|
end
|
72
85
|
end # Configuration
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Supervision
|
4
|
+
# A class responsible for measuring increments/decrements of value
|
5
|
+
class Counter
|
6
|
+
# Create a Counter
|
7
|
+
#
|
8
|
+
# @api public
|
9
|
+
def initialize
|
10
|
+
@count = Atomic.new(0)
|
11
|
+
end
|
12
|
+
|
13
|
+
# Reset the counter
|
14
|
+
#
|
15
|
+
# @return [nil]
|
16
|
+
#
|
17
|
+
# @api public
|
18
|
+
def clear
|
19
|
+
@count.set(0)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Increment counter
|
23
|
+
#
|
24
|
+
# @return [nil]
|
25
|
+
#
|
26
|
+
# @api public
|
27
|
+
def increment(incr = 1)
|
28
|
+
@count.update { |v| v + incr }
|
29
|
+
end
|
30
|
+
|
31
|
+
# Decrement counter
|
32
|
+
#
|
33
|
+
# @param []
|
34
|
+
#
|
35
|
+
# @return [nil]
|
36
|
+
#
|
37
|
+
# @api public
|
38
|
+
def decrement(decr = 1)
|
39
|
+
@count.update { |v| v + decr }
|
40
|
+
end
|
41
|
+
|
42
|
+
# Return the value
|
43
|
+
#
|
44
|
+
# @api public
|
45
|
+
def value
|
46
|
+
@count.value
|
47
|
+
end
|
48
|
+
end # Counter
|
49
|
+
end # Supervision
|
data/lib/supervision/registry.rb
CHANGED
@@ -3,7 +3,6 @@
|
|
3
3
|
module Supervision
|
4
4
|
# A class responsible for registering/unregistering circuits
|
5
5
|
class Registry
|
6
|
-
|
7
6
|
# Initialize a Registry
|
8
7
|
#
|
9
8
|
# @api public
|
@@ -14,10 +13,19 @@ module Supervision
|
|
14
13
|
|
15
14
|
# Register a circuit
|
16
15
|
#
|
16
|
+
# @param [String] name
|
17
|
+
# the name under which to register
|
18
|
+
#
|
19
|
+
# @param [Supervision::CircuitBreaker] circuit
|
20
|
+
# the registered circuit breaker
|
21
|
+
#
|
17
22
|
# @api public
|
18
23
|
def []=(name, circuit)
|
19
24
|
unless circuit.is_a?(CircuitBreaker)
|
20
|
-
raise TypeError, 'not a circuit'
|
25
|
+
raise TypeError, 'not a type of circuit breaker'
|
26
|
+
end
|
27
|
+
if registered?(name)
|
28
|
+
raise DuplicateEntryError, "`#{name}` is already registered"
|
21
29
|
end
|
22
30
|
@lock.synchronize do
|
23
31
|
@map[name.to_sym] = circuit
|
@@ -26,6 +34,8 @@ module Supervision
|
|
26
34
|
|
27
35
|
# Retrieve a circuit by name
|
28
36
|
#
|
37
|
+
# @param [String] name
|
38
|
+
#
|
29
39
|
# @api public
|
30
40
|
def [](name)
|
31
41
|
@lock.synchronize do
|
@@ -48,15 +58,39 @@ module Supervision
|
|
48
58
|
|
49
59
|
# Check if circuit is in registry
|
50
60
|
#
|
61
|
+
# @return [Boolean]
|
62
|
+
#
|
51
63
|
# @api public
|
52
64
|
def registered?(name)
|
53
|
-
names.include?(name)
|
65
|
+
names.include?(name) || names.include?(name.to_sym)
|
54
66
|
end
|
55
67
|
|
68
|
+
# Retrieve registered circuits' names
|
69
|
+
#
|
70
|
+
# @return [Array]
|
71
|
+
#
|
72
|
+
# @api public
|
56
73
|
def names
|
57
74
|
@lock.synchronize { @map.keys }
|
58
75
|
end
|
59
76
|
|
77
|
+
# Check if registry is empty or not
|
78
|
+
#
|
79
|
+
# @return [Boolean]
|
80
|
+
#
|
81
|
+
# @api public
|
82
|
+
def empty?
|
83
|
+
@lock.synchronize { @map.empty? }
|
84
|
+
end
|
85
|
+
|
86
|
+
# Remove all registered circuits
|
87
|
+
#
|
88
|
+
# @example
|
89
|
+
# registry.clear
|
90
|
+
#
|
91
|
+
# @return [Hash]
|
92
|
+
#
|
93
|
+
# @api public
|
60
94
|
def clear
|
61
95
|
hash = nil
|
62
96
|
@lock.synchronize do
|
data/lib/supervision/version.rb
CHANGED
data/spec/spec_helper.rb
CHANGED
@@ -1,5 +1,20 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
|
3
|
+
if RUBY_VERSION > '1.9' and (ENV['COVERAGE'] || ENV['TRAVIS'])
|
4
|
+
require 'simplecov'
|
5
|
+
require 'coveralls'
|
6
|
+
|
7
|
+
SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
|
8
|
+
SimpleCov::Formatter::HTMLFormatter,
|
9
|
+
Coveralls::SimpleCov::Formatter
|
10
|
+
]
|
11
|
+
|
12
|
+
SimpleCov.start do
|
13
|
+
command_name 'spec'
|
14
|
+
add_filter 'spec'
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
3
18
|
require 'supervision'
|
4
19
|
require 'timeout'
|
5
20
|
|
@@ -9,6 +24,16 @@ module Helpers
|
|
9
24
|
sleep(duration || 0.01) until yield
|
10
25
|
end
|
11
26
|
end
|
27
|
+
|
28
|
+
def spawn(threads = 2)
|
29
|
+
threads.times.map do |i|
|
30
|
+
Thread.new do
|
31
|
+
yield i
|
32
|
+
end
|
33
|
+
end.each do |thread|
|
34
|
+
thread.join
|
35
|
+
end
|
36
|
+
end
|
12
37
|
end
|
13
38
|
|
14
39
|
RSpec.configure do |config|
|
@@ -12,17 +12,34 @@ describe Supervision::CircuitBreaker do
|
|
12
12
|
|
13
13
|
let(:object) { described_class }
|
14
14
|
|
15
|
+
it "fails without a block" do
|
16
|
+
expect { object.new }.to raise_error(Supervision::InvalidParameterError)
|
17
|
+
end
|
18
|
+
|
19
|
+
it "resets circuit" do
|
20
|
+
circuit = object.new { }
|
21
|
+
expect(circuit.control).to receive(:reset!)
|
22
|
+
circuit.reset!
|
23
|
+
end
|
24
|
+
|
25
|
+
it "configures the control" do
|
26
|
+
block = -> { }
|
27
|
+
circuit = object.new { }
|
28
|
+
expect(circuit.control.config).to receive(:configure).with(&block)
|
29
|
+
circuit.configure(&block)
|
30
|
+
end
|
15
31
|
|
16
32
|
context 'when closed' do
|
17
33
|
it "successfully calls the method" do
|
18
34
|
circuit = object.new call_timeout: 1.milli do |arg|
|
19
35
|
arg == :danger ? dangerouse_call_error : safe_call
|
20
36
|
end
|
37
|
+
expect(circuit.current).to be(:closed)
|
21
38
|
expect(circuit.call(:safe)).to eql(safe_call)
|
22
39
|
end
|
23
40
|
|
24
41
|
it "increments a failure counter for exceptions" do
|
25
|
-
circuit = object.new call_timeout: 1.milli do
|
42
|
+
circuit = object.new call_timeout: 1.milli do |arg|
|
26
43
|
arg == :danger ? dangerouse_call_error : safe_call
|
27
44
|
end
|
28
45
|
circuit.call(:danger)
|
@@ -49,39 +66,39 @@ describe Supervision::CircuitBreaker do
|
|
49
66
|
end
|
50
67
|
|
51
68
|
it "enters a :half_open state after the :reset_timeout" do
|
52
|
-
circuit = object.new reset_timeout: 0.
|
69
|
+
circuit = object.new reset_timeout: 0.2.sec, max_failures: 0 do
|
53
70
|
dangerous_call_error
|
54
71
|
end
|
55
72
|
expect { circuit.call }.to raise_error(Supervision::CircuitBreakerOpenError)
|
56
73
|
expect(circuit.control.current).to eq(:open)
|
57
|
-
sleep 0.
|
74
|
+
sleep 0.4
|
58
75
|
expect(circuit.control.current).to eq(:half_open)
|
59
76
|
end
|
60
77
|
end
|
61
78
|
|
62
79
|
context 'when half open' do
|
63
80
|
it "resets the breaker back to :closed state on successful call" do
|
64
|
-
circuit = object.new reset_timeout:
|
81
|
+
circuit = object.new reset_timeout: 0.2.sec, max_failures: 0 do |arg|
|
65
82
|
arg == :danger ? dangerous_call_error : safe_call
|
66
83
|
end
|
67
84
|
expect {
|
68
85
|
circuit.call(:danger)
|
69
86
|
}.to raise_error(Supervision::CircuitBreakerOpenError)
|
70
87
|
expect(circuit.control.current).to eql(:open)
|
71
|
-
sleep 0.
|
88
|
+
sleep 0.4
|
72
89
|
expect(circuit.control.current).to eql(:half_open)
|
73
90
|
circuit.call(:safe)
|
74
91
|
expect(circuit.control.current).to eql(:closed)
|
75
92
|
end
|
76
93
|
end
|
77
94
|
|
78
|
-
context '
|
95
|
+
context 'with notification' do
|
79
96
|
it "notifies about successful call" do
|
80
97
|
callbacks = []
|
81
98
|
circuit = object.new do safe_call end
|
82
99
|
circuit.on_success { callbacks << 'on_success' }
|
83
100
|
circuit.call
|
84
|
-
expect(callbacks).to
|
101
|
+
expect(callbacks).to match_array(["on_success"])
|
85
102
|
end
|
86
103
|
|
87
104
|
it "notifies about failed call" do
|
@@ -90,13 +107,21 @@ describe Supervision::CircuitBreaker do
|
|
90
107
|
circuit.on_failure { callbacks << 'on_failure'}
|
91
108
|
circuit.before { callbacks << 'before'}
|
92
109
|
circuit.call
|
93
|
-
expect(callbacks).to
|
110
|
+
expect(callbacks).to match_array(['before', 'on_failure'])
|
94
111
|
end
|
95
112
|
end
|
96
113
|
|
97
|
-
|
98
|
-
|
99
|
-
object.new
|
100
|
-
|
114
|
+
describe "#to_s" do
|
115
|
+
it 'prints object info' do
|
116
|
+
circuit = object.new(name: :danger) { }
|
117
|
+
expect(circuit.to_s).to include("@name=danger")
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
describe "#inspect" do
|
122
|
+
it 'prints object info' do
|
123
|
+
circuit = object.new(name: :danger) { }
|
124
|
+
expect(circuit.inspect).to include("@name=danger")
|
125
|
+
end
|
101
126
|
end
|
102
127
|
end
|
@@ -7,7 +7,7 @@ describe Supervision::CircuitControl do
|
|
7
7
|
|
8
8
|
let(:max_failures) { 1 }
|
9
9
|
|
10
|
-
let(:reset_timeout) { 0.
|
10
|
+
let(:reset_timeout) { 0.2.sec }
|
11
11
|
|
12
12
|
subject(:control) {
|
13
13
|
object.new max_failures: max_failures,
|
@@ -17,56 +17,87 @@ describe Supervision::CircuitControl do
|
|
17
17
|
context 'when closed' do
|
18
18
|
it "resets the failure count on success" do
|
19
19
|
expect(control.failure_count).to eql(0)
|
20
|
-
expect(control.
|
20
|
+
expect(control.current).to eql(:closed)
|
21
21
|
control.record_failure
|
22
22
|
expect(control.failure_count).to eql(1)
|
23
23
|
control.reset_failure
|
24
24
|
expect(control.failure_count).to eql(0)
|
25
|
-
expect(control.
|
25
|
+
expect(control.current).to eql(:closed)
|
26
26
|
end
|
27
27
|
|
28
28
|
it "increments failure count on exceptions and trips the wire" do
|
29
29
|
expect(control.failure_count).to eql(0)
|
30
|
-
expect(control.
|
30
|
+
expect(control.current).to eql(:closed)
|
31
31
|
|
32
|
-
control.
|
32
|
+
control.handle_failure
|
33
33
|
expect(control.failure_count).to eql(1)
|
34
|
-
expect(control.
|
34
|
+
expect(control.current).to eql(:closed)
|
35
35
|
|
36
|
-
expect{
|
36
|
+
expect {
|
37
|
+
control.handle_failure
|
38
|
+
}.to raise_error(Supervision::CircuitBreakerOpenError)
|
37
39
|
expect(control.failure_count).to eql(2)
|
38
|
-
expect(control.
|
40
|
+
expect(control.current).to eql(:open)
|
39
41
|
end
|
40
42
|
end
|
41
43
|
|
42
44
|
context 'when open' do
|
43
45
|
it "fails all calls fast with CircuitBreakerOpenError" do
|
44
|
-
control.
|
45
|
-
expect {
|
46
|
-
|
46
|
+
control.trip!
|
47
|
+
expect {
|
48
|
+
control.handle_failure
|
49
|
+
}.to raise_error(Supervision::CircuitBreakerOpenError)
|
50
|
+
expect(control.current).to eq(:open)
|
47
51
|
end
|
48
52
|
|
49
53
|
it "enters :half_open state after the configured :reset_timeout" do
|
50
54
|
control.record_failure
|
51
|
-
expect{
|
52
|
-
|
53
|
-
|
55
|
+
expect {
|
56
|
+
control.handle_failure
|
57
|
+
}.to raise_error(Supervision::CircuitBreakerOpenError)
|
58
|
+
sleep 2 * reset_timeout
|
59
|
+
expect(control.current).to eq(:half_open)
|
54
60
|
end
|
55
61
|
end
|
56
62
|
|
57
63
|
context 'when half open' do
|
58
|
-
before { control.
|
64
|
+
before { control.attempt_reset! }
|
59
65
|
|
60
66
|
it "resets the breaker back to :closed state on successful call" do
|
67
|
+
expect(control.current).to eq(:half_open)
|
61
68
|
control.record_success
|
62
|
-
expect(control.
|
69
|
+
expect(control.current).to eql(:closed)
|
63
70
|
expect(control.failure_count).to eql(0)
|
64
71
|
end
|
65
72
|
|
66
73
|
it "trips the breaker back to :open state on failed call" do
|
67
|
-
expect
|
74
|
+
expect(control.current).to eq(:half_open)
|
75
|
+
expect {
|
76
|
+
control.handle_failure
|
77
|
+
}.to raise_error(Supervision::CircuitBreakerOpenError)
|
68
78
|
expect(control.failure_count).to eql(1)
|
69
|
-
expect(control.
|
79
|
+
expect(control.current).to eql(:open)
|
70
80
|
end
|
71
81
|
end
|
82
|
+
|
83
|
+
describe "#measure_timeout" do
|
84
|
+
it "kills the scheduler thread" do
|
85
|
+
expect {
|
86
|
+
control.trip
|
87
|
+
}.to raise_error(Supervision::CircuitBreakerOpenError)
|
88
|
+
expect(control.current).to eq(:open)
|
89
|
+
expect(control.scheduler).to receive(:kill).once
|
90
|
+
control.stub(:max_thread_lifetime).and_return 0
|
91
|
+
sleep reset_timeout
|
92
|
+
expect(control.current).to eq(:open)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
it "forces reset to closed state" do
|
97
|
+
control.attempt_reset!
|
98
|
+
expect(control.current).to eq(:half_open)
|
99
|
+
expect(control).to receive(:reset_failure)
|
100
|
+
control.reset!
|
101
|
+
expect(control.current).to eq(:closed)
|
102
|
+
end
|
72
103
|
end
|