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