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