supervision 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|