supervision 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,69 @@
1
+ # encoding: utf-8
2
+
3
+ module Supervision
4
+ # A class responsible for registering/unregistering circuits
5
+ class Registry
6
+
7
+ # Initialize a Registry
8
+ #
9
+ # @api public
10
+ def initialize
11
+ @lock = Mutex.new
12
+ @map = {}
13
+ end
14
+
15
+ # Register a circuit
16
+ #
17
+ # @api public
18
+ def []=(name, circuit)
19
+ unless circuit.is_a?(CircuitBreaker)
20
+ raise TypeError, 'not a circuit'
21
+ end
22
+ @lock.synchronize do
23
+ @map[name.to_sym] = circuit
24
+ end
25
+ end
26
+
27
+ # Retrieve a circuit by name
28
+ #
29
+ # @api public
30
+ def [](name)
31
+ @lock.synchronize do
32
+ @map[name.to_sym]
33
+ end
34
+ end
35
+
36
+ # Remove from registry
37
+ #
38
+ # @api public
39
+ def delete(name)
40
+ @lock.synchronize do
41
+ @map.delete name.to_sym
42
+ end
43
+ end
44
+
45
+ alias_method :register, :[]=
46
+ alias_method :get, :[]
47
+ alias_method :unregister, :delete
48
+
49
+ # Check if circuit is in registry
50
+ #
51
+ # @api public
52
+ def registered?(name)
53
+ names.include?(name)
54
+ end
55
+
56
+ def names
57
+ @lock.synchronize { @map.keys }
58
+ end
59
+
60
+ def clear
61
+ hash = nil
62
+ @lock.synchronize do
63
+ hash = @map.dup
64
+ @map.clear
65
+ end
66
+ hash
67
+ end
68
+ end # Registry
69
+ end # Supervision
@@ -0,0 +1,36 @@
1
+ # encoding: utf-8
2
+
3
+ module Supervision
4
+ # A mixin to define time related helpers
5
+ module TimeDSL
6
+ def millisecond
7
+ self / 1000.0
8
+ end
9
+ alias_method :milliseconds, :millisecond
10
+ alias_method :milli , :millisecond
11
+ alias_method :millis , :millisecond
12
+
13
+ def second
14
+ self * 1
15
+ end
16
+ alias_method :seconds, :second
17
+ alias_method :sec , :second
18
+ alias_method :secs , :second
19
+
20
+ def minute
21
+ self * 60
22
+ end
23
+ alias_method :minutes, :minute
24
+ alias_method :min , :minute
25
+ alias_method :mins , :minute
26
+
27
+ def hour
28
+ self * 3600
29
+ end
30
+ alias_method :hours, :hour
31
+ end # TimeDSL
32
+ end # Supervision
33
+
34
+ unless Numeric.method_defined?(:second)
35
+ Numeric.send(:include, Supervision::TimeDSL)
36
+ end
@@ -0,0 +1,5 @@
1
+ # encoding: utf-8
2
+
3
+ module Supervision
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,67 @@
1
+ # encoding: utf-8
2
+
3
+ require "thread"
4
+ require "timeout"
5
+ require "finite_machine"
6
+
7
+ require "supervision/version"
8
+ require "supervision/atomic"
9
+ require "supervision/time_dsl"
10
+ require "supervision/configuration"
11
+ require "supervision/registry"
12
+ require "supervision/circuit_control"
13
+ require "supervision/circuit_breaker"
14
+ require "supervision/circuit_system"
15
+ require "supervision/circuit_monitor"
16
+
17
+ module Supervision
18
+ # Generic error
19
+ SupervisionError = Class.new(::StandardError)
20
+
21
+ # Raised when circuit opens
22
+ CircuitBreakerOpenError = Class.new(SupervisionError)
23
+
24
+ TypeError = Class.new(SupervisionError)
25
+
26
+ class << self
27
+ def included(base)
28
+ base.send :extend, ClassMethods
29
+ end
30
+
31
+ def configuration
32
+ @configuration ||= Configuration.new
33
+ end
34
+
35
+ def init
36
+ @circuit_system = CircuitSystem.new
37
+ end
38
+
39
+ def circuit_system
40
+ Thread.current[:supervision_circuit_system] ||= @circuit_system
41
+ end
42
+
43
+ # Create a new circuit breaker
44
+ #
45
+ # @api public
46
+ def new(name = nil, options = {}, &block)
47
+ name ? supervise_as(name, options, &block) : supervise(options, &block)
48
+ end
49
+ end
50
+
51
+ module ClassMethods
52
+ def supervise(options = {}, &block)
53
+ CircuitBreaker.new(options, &block)
54
+ end
55
+
56
+ def supervise_as(name, options = {}, &block)
57
+ circuit = supervise(options, &block)
58
+ Supervision.circuit_system[name] = circuit
59
+ send(:define_method, name) { |*args| circuit.call(args) }
60
+ circuit
61
+ end
62
+ end
63
+
64
+ extend ClassMethods
65
+ end # Supervision
66
+
67
+ Supervision.init
@@ -0,0 +1,20 @@
1
+ # encoding: utf-8
2
+
3
+ require 'supervision'
4
+ require 'timeout'
5
+
6
+ module Helpers
7
+ def wait_for(duration = nil)
8
+ Timeout.timeout 1 do
9
+ sleep(duration || 0.01) until yield
10
+ end
11
+ end
12
+ end
13
+
14
+ RSpec.configure do |config|
15
+ config.treat_symbols_as_metadata_keys_with_true_values = true
16
+ config.run_all_when_everything_filtered = true
17
+ config.filter_run :focus
18
+ config.order = 'random'
19
+ config.include Helpers
20
+ end
@@ -0,0 +1,30 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Supervision::Atomic do
6
+
7
+ let(:object) { described_class }
8
+
9
+ it "sets the value to nil" do
10
+ atomic = object.new
11
+ expect(atomic.value).to eql(nil)
12
+ end
13
+
14
+ it "sets the value" do
15
+ atomic = object.new(1)
16
+ expect(atomic.value).to eql(1)
17
+ end
18
+
19
+ it "set value" do
20
+ atomic = object.new
21
+ atomic.value = 1000
22
+ expect(atomic.value).to eql(1000)
23
+ end
24
+
25
+ it "updates current value" do
26
+ atomic = object.new(1000)
27
+ new_value = atomic.update { |v| v + 1 }
28
+ expect(new_value).to eql(1001)
29
+ end
30
+ end
@@ -0,0 +1,102 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Supervision::CircuitBreaker do
6
+
7
+ let(:dangerous_call_timeout) { sleep 1 }
8
+
9
+ let(:dangerous_call_error) { raise StandardError }
10
+
11
+ let(:safe_call) { 'value' }
12
+
13
+ let(:object) { described_class }
14
+
15
+
16
+ context 'when closed' do
17
+ it "successfully calls the method" do
18
+ circuit = object.new call_timeout: 1.milli do |arg|
19
+ arg == :danger ? dangerouse_call_error : safe_call
20
+ end
21
+ expect(circuit.call(:safe)).to eql(safe_call)
22
+ end
23
+
24
+ it "increments a failure counter for exceptions" do
25
+ circuit = object.new call_timeout: 1.milli do
26
+ arg == :danger ? dangerouse_call_error : safe_call
27
+ end
28
+ circuit.call(:danger)
29
+ expect(circuit.control.failure_count).to eql(1)
30
+ end
31
+
32
+ it "increments a failure counter for calls exceeding :call_timeout" do
33
+ circuit = object.new call_timeout: 1.milli do
34
+ dangerous_call_timeout
35
+ end
36
+ circuit.call
37
+ expect(circuit.control.failure_count).to eql(1)
38
+ end
39
+ end
40
+
41
+ context 'when open' do
42
+ it "fails all calls with a CircuitBreakerOpenError" do
43
+ circuit = object.new max_failures: 2, reset_timeout: 1.sec do
44
+ dangerous_call_error
45
+ end
46
+ circuit.call
47
+ circuit.call
48
+ expect { circuit.call }.to raise_error(Supervision::CircuitBreakerOpenError)
49
+ end
50
+
51
+ it "enters a :half_open state after the :reset_timeout" do
52
+ circuit = object.new reset_timeout: 0.1.sec, max_failures: 0 do
53
+ dangerous_call_error
54
+ end
55
+ expect { circuit.call }.to raise_error(Supervision::CircuitBreakerOpenError)
56
+ expect(circuit.control.current).to eq(:open)
57
+ sleep 0.2
58
+ expect(circuit.control.current).to eq(:half_open)
59
+ end
60
+ end
61
+
62
+ context 'when half open' do
63
+ 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|
65
+ arg == :danger ? dangerous_call_error : safe_call
66
+ end
67
+ expect {
68
+ circuit.call(:danger)
69
+ }.to raise_error(Supervision::CircuitBreakerOpenError)
70
+ expect(circuit.control.current).to eql(:open)
71
+ sleep 0.2
72
+ expect(circuit.control.current).to eql(:half_open)
73
+ circuit.call(:safe)
74
+ expect(circuit.control.current).to eql(:closed)
75
+ end
76
+ end
77
+
78
+ context 'when with callback' do
79
+ it "notifies about successful call" do
80
+ callbacks = []
81
+ circuit = object.new do safe_call end
82
+ circuit.on_success { callbacks << 'on_success' }
83
+ circuit.call
84
+ expect(callbacks).to eql(["on_success"])
85
+ end
86
+
87
+ it "notifies about failed call" do
88
+ callbacks = []
89
+ circuit = object.new do dangerous_call_error end
90
+ circuit.on_failure { callbacks << 'on_failure'}
91
+ circuit.before { callbacks << 'before'}
92
+ circuit.call
93
+ expect(callbacks).to eql(['before', 'on_failure'])
94
+ end
95
+ end
96
+
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)
101
+ end
102
+ end
@@ -0,0 +1,72 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Supervision::CircuitControl do
6
+ let(:object) { described_class }
7
+
8
+ let(:max_failures) { 1 }
9
+
10
+ let(:reset_timeout) { 0.1.sec }
11
+
12
+ subject(:control) {
13
+ object.new max_failures: max_failures,
14
+ reset_timeout: reset_timeout
15
+ }
16
+
17
+ context 'when closed' do
18
+ it "resets the failure count on success" do
19
+ expect(control.failure_count).to eql(0)
20
+ expect(control.fsm.current).to eql(:closed)
21
+ control.record_failure
22
+ expect(control.failure_count).to eql(1)
23
+ control.reset_failure
24
+ expect(control.failure_count).to eql(0)
25
+ expect(control.fsm.current).to eql(:closed)
26
+ end
27
+
28
+ it "increments failure count on exceptions and trips the wire" do
29
+ expect(control.failure_count).to eql(0)
30
+ expect(control.fsm.current).to eql(:closed)
31
+
32
+ control.handle
33
+ expect(control.failure_count).to eql(1)
34
+ expect(control.fsm.current).to eql(:closed)
35
+
36
+ expect{ control.handle }.to raise_error(Supervision::CircuitBreakerOpenError)
37
+ expect(control.failure_count).to eql(2)
38
+ expect(control.fsm.current).to eql(:open)
39
+ end
40
+ end
41
+
42
+ context 'when open' do
43
+ 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)
47
+ end
48
+
49
+ it "enters :half_open state after the configured :reset_timeout" do
50
+ 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)
54
+ end
55
+ end
56
+
57
+ context 'when half open' do
58
+ before { control.fsm.state = :half_open }
59
+
60
+ it "resets the breaker back to :closed state on successful call" do
61
+ control.record_success
62
+ expect(control.fsm.current).to eql(:closed)
63
+ expect(control.failure_count).to eql(0)
64
+ end
65
+
66
+ it "trips the breaker back to :open state on failed call" do
67
+ expect { control.handle }.to raise_error(Supervision::CircuitBreakerOpenError)
68
+ expect(control.failure_count).to eql(1)
69
+ expect(control.fsm.current).to eql(:open)
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,6 @@
1
+ # encoding: utf-8
2
+
3
+ require "spec_helper"
4
+
5
+ describe Supervision::CircuitMonitor do
6
+ end
@@ -0,0 +1,14 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Supervision::Configuration do
6
+
7
+ subject(:config) { described_class.new }
8
+
9
+ it { expect(config.max_failures).to eql(5) }
10
+
11
+ it { expect(config.call_timeout).to eql(0.01) }
12
+
13
+ it { expect(config.reset_timeout).to eql(0.1) }
14
+ end
@@ -0,0 +1,55 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Supervision do
6
+
7
+ context "when used as instance" do
8
+ it "permits options configuration" do
9
+ supervision = Supervision.new { }
10
+ supervision.configure do
11
+ call_timeout 1.sec
12
+ max_failures 10
13
+ end
14
+ expect(supervision.control.max_failures).to eql(10)
15
+ expect(supervision.control.call_timeout).to eql(1.sec)
16
+ end
17
+
18
+ it "allows to supervise call" do
19
+ called = []
20
+ supervision = Supervision.supervise { called << 'method_call'}
21
+ supervision.call
22
+ expect(called).to eql(['method_call'])
23
+ end
24
+
25
+ it "registers named supervision" do
26
+ called = []
27
+ supervision = Supervision.supervise_as(:danger) { called << 'method_call'}
28
+ supervision.call
29
+ expect(called).to eql(['method_call'])
30
+ expect(Supervision.circuit_system[:danger]).to eql(supervision)
31
+ end
32
+ end
33
+
34
+ context "when included as module" do
35
+ class RemoteApi
36
+ include Supervision
37
+
38
+ def danger_call(state)
39
+ state == :safe ? "hello" : raise(StandardError)
40
+ end
41
+ supervise_as(:danger, max_failures: 2) { |args| danger_call(args) }
42
+
43
+ def wrapped_danger
44
+ supervise { |args| danger_call(args) }
45
+ end
46
+ end
47
+
48
+ it "allows to call registerd circuit" do
49
+ api = RemoteApi.new
50
+ api.danger
51
+ api.danger
52
+ expect { api.danger }.to raise_error(Supervision::CircuitBreakerOpenError)
53
+ end
54
+ end
55
+ end # Supervision
@@ -0,0 +1,30 @@
1
+ require 'spec_helper'
2
+
3
+ describe Supervision::Registry do
4
+
5
+ let(:circuit) { Supervision.supervise { } }
6
+
7
+ subject(:registry) { described_class.new }
8
+
9
+ it "registers" do
10
+ registry[:danger] = circuit
11
+ expect(registry[:danger]).to eql(circuit)
12
+ end
13
+
14
+ it "refuses to add non circuit object" do
15
+ expect {
16
+ registry[:danger] = Object.new
17
+ }.to raise_error(Supervision::TypeError)
18
+ end
19
+
20
+ it "" do
21
+ registry[:danger] = circuit
22
+ expect(registry.names).to eql([:danger])
23
+ end
24
+
25
+ it "" do
26
+ registry[:danger] = circuit
27
+ registry.delete(:danger)
28
+ expect(registry.names).to be_empty
29
+ end
30
+ end
@@ -0,0 +1,31 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Supervision::TimeDSL do
6
+ it "defines millisecond/milliseconds/milli/millis" do
7
+ expect(1.millisecond).to eql(0.001)
8
+ expect(1.milli).to eql(0.001)
9
+ expect(10.milliseconds).to eql(0.01)
10
+ expect(10.millis).to eql(0.01)
11
+ end
12
+
13
+ it "defines second/seconds" do
14
+ expect(1.second).to eql(1)
15
+ expect(10.seconds).to eql(10)
16
+ expect(1.sec).to eql(1)
17
+ expect(10.secs).to eql(10)
18
+ end
19
+
20
+ it "defines minute/minutes" do
21
+ expect(1.minute).to eql(60)
22
+ expect(10.minutes).to eql(600)
23
+ expect(1.min).to eql(60)
24
+ expect(10.mins).to eql(600)
25
+ end
26
+
27
+ it "defines hours/hours" do
28
+ expect(1.hour).to eql(3600)
29
+ expect(2.hours).to eql(7200)
30
+ end
31
+ end
@@ -0,0 +1,23 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'supervision/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "supervision"
8
+ spec.version = Supervision::VERSION
9
+ spec.authors = ["Piotr Murach"]
10
+ spec.email = [""]
11
+ spec.summary = %q{Write distributed systems that are resilient and self-heal.}
12
+ spec.description = %q{Write distributed systems that are resilient and self-heal. Remote calls can fail or hang indefinietly without a response. Supervision will help to isolate failure and keep individual components from bringing down the whole system.}
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "finite_machine", "~> 0.4"
22
+ spec.add_development_dependency "bundler", "~> 1.6"
23
+ end
@@ -0,0 +1,10 @@
1
+ # encoding: utf-8
2
+
3
+ desc 'Load gem inside irb console'
4
+ task :console do
5
+ require 'irb'
6
+ require 'irb/completion'
7
+ require File.join(__FILE__, '../lib/finite_machine')
8
+ ARGV.clear
9
+ IRB.start
10
+ end
@@ -0,0 +1,11 @@
1
+ # encoding: utf-8
2
+
3
+ desc 'Measure code coverage'
4
+ task :coverage do
5
+ begin
6
+ original, ENV['COVERAGE'] = ENV['COVERAGE'], 'true'
7
+ Rake::Task['spec'].invoke
8
+ ensure
9
+ ENV['COVERAGE'] = original
10
+ end
11
+ end
data/tasks/spec.rake ADDED
@@ -0,0 +1,29 @@
1
+ # encoding: utf-8
2
+
3
+ begin
4
+ require 'rspec/core/rake_task'
5
+
6
+ desc 'Run all specs'
7
+ RSpec::Core::RakeTask.new(:spec) do |task|
8
+ task.pattern = 'spec/{unit,integration}{,/*/**}/*_spec.rb'
9
+ end
10
+
11
+ namespace :spec do
12
+ desc 'Run unit specs'
13
+ RSpec::Core::RakeTask.new(:unit) do |task|
14
+ task.pattern = 'spec/unit{,/*/**}/*_spec.rb'
15
+ end
16
+
17
+ desc 'Run integration specs'
18
+ RSpec::Core::RakeTask.new(:integration) do |task|
19
+ task.pattern = 'spec/integration{,/*/**}/*_spec.rb'
20
+ end
21
+ end
22
+
23
+ rescue LoadError
24
+ %w[spec spec:unit spec:integration].each do |name|
25
+ task name do
26
+ $stderr.puts "In order to run #{name}, do `gem install rspec`"
27
+ end
28
+ end
29
+ end