supervision 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
- def alert(type)
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
- def_delegators '@registry', :[], :get, :[]=, :set,
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 ArgumentError, "`#{option}` isn`t recognized as valid parameter." \
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
@@ -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
@@ -1,5 +1,5 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  module Supervision
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
@@ -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.1.sec, max_failures: 0 do
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.2
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: 100.milli, max_failures: 0 do |arg|
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.2
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 'when with callback' do
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 eql(["on_success"])
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 eql(['before', 'on_failure'])
110
+ expect(callbacks).to match_array(['before', 'on_failure'])
94
111
  end
95
112
  end
96
113
 
97
- it "fails fast with unknown config option" do
98
- expect {
99
- object.new max_fail: 2 do safe_call end
100
- }.to raise_error(ArgumentError)
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.1.sec }
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.fsm.current).to eql(:closed)
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.fsm.current).to eql(:closed)
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.fsm.current).to eql(:closed)
30
+ expect(control.current).to eql(:closed)
31
31
 
32
- control.handle
32
+ control.handle_failure
33
33
  expect(control.failure_count).to eql(1)
34
- expect(control.fsm.current).to eql(:closed)
34
+ expect(control.current).to eql(:closed)
35
35
 
36
- expect{ control.handle }.to raise_error(Supervision::CircuitBreakerOpenError)
36
+ expect {
37
+ control.handle_failure
38
+ }.to raise_error(Supervision::CircuitBreakerOpenError)
37
39
  expect(control.failure_count).to eql(2)
38
- expect(control.fsm.current).to eql(:open)
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.fsm.state = :open
45
- expect { control.handle }.to raise_error(Supervision::CircuitBreakerOpenError)
46
- expect(control.fsm.current).to eql(:open)
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{ control.handle }.to raise_error(Supervision::CircuitBreakerOpenError)
52
- sleep 0.2
53
- expect(control.fsm.current).to eql(:half_open)
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.fsm.state = :half_open }
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.fsm.current).to eql(:closed)
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 { control.handle }.to raise_error(Supervision::CircuitBreakerOpenError)
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.fsm.current).to eql(:open)
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