circuit_breakage 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: dad99a3f4f0f6b939a2092e865ac7dca10b5fe4b
4
+ data.tar.gz: 6e7684487d5a883b407013767e8f6a454933c9fb
5
+ SHA512:
6
+ metadata.gz: 4895d56acb08b9c942e1f571162017b46b40da00b5872b5a5891d411dd5db3461d48d0f21c3ef8227efbf3aab48e3c7df10a1f79ea32e50b45cefc43a2b6d6cf
7
+ data.tar.gz: 3f9da79d01746dcf5e3d8e14ae4adc7eb3f831370ed1cbf55842d97eca744fd15f85662c572905038c7e4f7c61a38a141ab9a26c8de0df0c803bf6aa0fabb963
data/.gitignore ADDED
@@ -0,0 +1,22 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *.bundle
19
+ *.so
20
+ *.o
21
+ *.a
22
+ mkmf.log
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in circuit_breakage.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 John Hyland
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,20 @@
1
+ # CircuitBreakage
2
+
3
+ A simple Circuit Breaker implementation in Ruby with a timeout. A Circuit
4
+ Breaker wraps potentially troublesome logic and will "trip" the circuit (and
5
+ stop trying to run the logic) if it sees too many failures. After a while, it
6
+ will retry.
7
+
8
+ ## Usage
9
+
10
+ ```ruby
11
+ block = ->(*args) do
12
+ # Some dangerous thing.
13
+ end
14
+
15
+ breaker = CircuitBreakage.new(block)
16
+ breaker.failure_threshold = 3 # only 3 failures before tripping circuit
17
+ breaker.duration = 10 # 10 seconds before retry
18
+ breaker.timeout = 0.5 # 500 milliseconds allowed before auto-fail
19
+
20
+ breaker.call(*some_args) # args are passed through to block
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -0,0 +1,25 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'circuit_breakage/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "circuit_breakage"
8
+ spec.version = CircuitBreakage::VERSION
9
+ spec.authors = ["John Hyland"]
10
+ spec.email = ["john@djspinmonkey.com"]
11
+ spec.summary = %q{Provides a simple circuit breaker pattern.}
12
+ spec.description = %q{Provides a circuit breaker pattern with configurable error tolerance, timeout, breakage duration, and state storage.}
13
+ spec.homepage = "https://source.datanerd.us/jhyland/circuit_breakage" # TODO: move to some org
14
+ spec.license = "New Relic"
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_development_dependency "bundler", "~> 1.6"
22
+ spec.add_development_dependency "rake", "~> 0"
23
+ spec.add_development_dependency "rspec", "~> 0"
24
+ spec.add_development_dependency "pry", "~> 0"
25
+ end
@@ -0,0 +1,78 @@
1
+ require 'timeout'
2
+
3
+ module CircuitBreakage
4
+ class CircuitOpen < RuntimeError; end
5
+ class CircuitTimeout < RuntimeError; end
6
+
7
+ # A simple circuit breaker implementation. See the main README for usage
8
+ # details.
9
+ #
10
+ class Breaker
11
+ attr_accessor :failure_count, :last_failed, :state, :block
12
+ attr_accessor :failure_threshold, :duration, :timeout
13
+
14
+ DEFAULT_FAILURE_THRESHOLD = 5 # Number of failures required to trip circuit
15
+ DEFAULT_DURATION = 300 # Number of seconds the circuit stays tripped
16
+ DEFAULT_TIMEOUT = 10 # Number of seconds before the call times out
17
+
18
+ def initialize(block)
19
+ @block = block
20
+ self.failure_threshold = DEFAULT_FAILURE_THRESHOLD
21
+ self.duration = DEFAULT_DURATION
22
+ self.timeout = DEFAULT_TIMEOUT
23
+
24
+ self.failure_count ||= 0
25
+ self.last_failed ||= Time.at(0)
26
+ closed!
27
+ end
28
+
29
+ def call(*args)
30
+ if open?
31
+ if time_to_retry?
32
+ half_open!
33
+ else
34
+ raise CircuitOpen
35
+ end
36
+ end
37
+
38
+ begin
39
+ ret_value = nil
40
+ Timeout.timeout(self.timeout, CircuitTimeout) do
41
+ ret_value = @block.call(*args)
42
+ end
43
+ handle_success
44
+
45
+ return ret_value
46
+ rescue Exception => e
47
+ handle_failure
48
+ end
49
+ end
50
+
51
+ [:open, :closed, :half_open].each do |state|
52
+ define_method("#{state}?") {
53
+ self.state == state
54
+ }
55
+
56
+ define_method("#{state}!") {
57
+ self.state = state
58
+ }
59
+ end
60
+
61
+ private
62
+
63
+ def time_to_retry?
64
+ Time.now >= self.last_failed + self.duration
65
+ end
66
+
67
+ def handle_success
68
+ closed!
69
+ self.failure_count = 0
70
+ end
71
+
72
+ def handle_failure
73
+ self.last_failed = Time.now
74
+ self.failure_count += 1
75
+ open! if self.failure_count >= self.failure_threshold
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,31 @@
1
+ module CircuitBreakage
2
+ # Similar to Breaker, but accepts a cache object, and will call #write and
3
+ # #fetch on that object to store and retrieve all state, instead of keeping
4
+ # it in memory.
5
+ #
6
+ class CachingBreaker < Breaker
7
+ attr_reader :cache, :key
8
+
9
+ def initialize(cache, key, block)
10
+ @cache = cache
11
+ @key = key
12
+ super(block)
13
+ end
14
+
15
+ def self.cached_attr(*attrs)
16
+ attrs.each do |attr|
17
+ define_method attr do
18
+ raise "You must define the cache and key on a CachingBreaker!" unless cache && key
19
+ cache.fetch "#{key}/#{attr}"
20
+ end
21
+
22
+ define_method "#{attr}=" do |value|
23
+ raise "You must define the cache and key on a CachingBreaker!" unless cache && key
24
+ cache.write "#{key}/#{attr}", value
25
+ end
26
+ end
27
+ end
28
+
29
+ cached_attr :failure_count, :last_failed, :state
30
+ end
31
+ end
@@ -0,0 +1,3 @@
1
+ module CircuitBreakage
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,5 @@
1
+ require "circuit_breakage/version"
2
+
3
+ module CircuitBreakage
4
+ # Your code goes here...
5
+ end
@@ -0,0 +1,125 @@
1
+ module CircuitBreakage
2
+ describe Breaker do
3
+ let(:breaker) { Breaker.new(block) }
4
+ let(:block) { ->(x) { return x } }
5
+
6
+ it 'initializes with a block' do
7
+ block = ->() { "This is a block!" }
8
+ breaker = Breaker.new(block)
9
+ expect(breaker).to be_a(Breaker)
10
+ end
11
+
12
+ describe '#call' do
13
+ subject { -> { breaker.call(arg) } }
14
+ let(:arg) { 'This is an argument.' }
15
+
16
+ context 'when the circuit is closed' do
17
+ before { breaker.closed! }
18
+
19
+ it 'calls the block' do
20
+ # The default block just returns the arg.
21
+ expect(breaker.call(arg)).to eq arg
22
+ end
23
+
24
+ context 'and the call succeeds' do
25
+ it 'resets the failure count' do
26
+ breaker.failure_count = 3
27
+ expect { breaker.call(arg) }.to change { breaker.failure_count }.to(0)
28
+ end
29
+ end
30
+
31
+ context 'and the call fails' do
32
+ let(:block) { -> { raise 'some error' } }
33
+
34
+ it { is_expected.to change { breaker.failure_count }.by(1) }
35
+ it { is_expected.to change { breaker.last_failed } }
36
+
37
+ context 'and the failure count exceeds the failure threshold' do
38
+ before { breaker.failure_count = breaker.failure_threshold }
39
+
40
+ it { is_expected.to change { breaker.open? }.to(true) }
41
+ end
42
+ end
43
+
44
+ context 'and the call times out' do
45
+ let(:block) { ->(_) { sleep 2 } }
46
+ before { breaker.timeout = 0.1 }
47
+
48
+ it 'counts as a failure' do
49
+ expect { breaker.call(arg) }.to change { breaker.failure_count }.by(1)
50
+ end
51
+ end
52
+ end
53
+
54
+ context 'when the circuit is open' do
55
+ before { breaker.open! }
56
+
57
+ context 'before the retry_time' do
58
+ before { breaker.last_failed = Time.now - breaker.duration + 30 }
59
+
60
+ it { is_expected.to raise_error(CircuitOpen) }
61
+ end
62
+
63
+ context 'after the retry time' do
64
+ before { breaker.last_failed = Time.now - breaker.duration - 30 }
65
+
66
+ it 'calls the block' do
67
+ # This is the same as being half open, see below for further tests.
68
+ expect(breaker.call(arg)).to eq arg
69
+ end
70
+ end
71
+ end
72
+
73
+ context 'when the circuit is half open' do
74
+ before do
75
+ # For the circuit to be tripped in the first place, the failure count
76
+ # must have reached the failure threshold.
77
+ breaker.failure_count = breaker.failure_threshold
78
+ breaker.half_open!
79
+ end
80
+
81
+ it 'calls the block' do
82
+ expect(breaker.call(arg)).to eq arg
83
+ end
84
+
85
+ context 'and the call succeeds' do
86
+ before { breaker.failure_count = 3 }
87
+
88
+ it { is_expected.to change { breaker.closed? }.to(true) }
89
+ it { is_expected.to change { breaker.failure_count }.to(0) }
90
+ end
91
+
92
+ context 'and the call fails' do
93
+ let(:block) { -> { raise 'some error' } }
94
+
95
+ it { is_expected.to change { breaker.open? }.to(true) }
96
+ it { is_expected.to change { breaker.last_failed } }
97
+ end
98
+ end
99
+ end
100
+
101
+ # #half_open?, #half_open!, #closed?, and #closed! are all exactly the same
102
+ # as #open? and #open!, so we're just going to test the open methods.
103
+
104
+ describe '#open!' do
105
+ it 'opens the circuit' do
106
+ breaker.open!
107
+ expect(breaker).to be_open
108
+ end
109
+ end
110
+
111
+ describe '#open?' do
112
+ subject { breaker.open? }
113
+
114
+ context 'when open' do
115
+ before { breaker.open! }
116
+ it { is_expected.to be_truthy }
117
+ end
118
+
119
+ context 'when not open' do
120
+ before { breaker.closed! }
121
+ it { is_expected.to be_falsey }
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,140 @@
1
+ # TODO: Extract the common tests (ie, most of them) in to a shared example
2
+ # group for this and the vanilla Breaker spec.
3
+
4
+ module CircuitBreakage
5
+ describe CachingBreaker do
6
+ let(:breaker) { CachingBreaker.new(cache, key, block) }
7
+ let(:cache) { MockCache.new }
8
+ let(:key) { 'test/data' }
9
+ let(:block) { ->(x) { return x } }
10
+
11
+ describe '#call' do
12
+ subject { -> { breaker.call(arg) } }
13
+ let(:arg) { 'This is an argument.' }
14
+
15
+ context 'when the circuit is closed' do
16
+ before { breaker.closed! }
17
+
18
+ it 'calls the block' do
19
+ # The default block just returns the arg.
20
+ expect(breaker.call(arg)).to eq arg
21
+ end
22
+
23
+ context 'and the call succeeds' do
24
+ it 'resets the failure count' do
25
+ breaker.failure_count = 3
26
+ expect { breaker.call(arg) }.to change { breaker.failure_count }.to(0)
27
+ end
28
+ end
29
+
30
+ context 'and the call fails' do
31
+ let(:block) { -> { raise 'some error' } }
32
+
33
+ it { is_expected.to change { breaker.failure_count }.by(1) }
34
+ it { is_expected.to change { breaker.last_failed } }
35
+
36
+ context 'and the failure count exceeds the failure threshold' do
37
+ before { breaker.failure_count = breaker.failure_threshold }
38
+
39
+ it { is_expected.to change { breaker.open? }.to(true) }
40
+ end
41
+ end
42
+
43
+ context 'and the call times out' do
44
+ let(:block) { ->(_) { sleep 2 } }
45
+ before { breaker.timeout = 0.1 }
46
+
47
+ it 'counts as a failure' do
48
+ expect { breaker.call(arg) }.to change { breaker.failure_count }.by(1)
49
+ end
50
+ end
51
+ end
52
+
53
+ context 'when the circuit is open' do
54
+ before { breaker.open! }
55
+
56
+ context 'before the retry_time' do
57
+ before { breaker.last_failed = Time.now - breaker.duration + 30 }
58
+
59
+ it { is_expected.to raise_error(CircuitOpen) }
60
+ end
61
+
62
+ context 'after the retry time' do
63
+ before { breaker.last_failed = Time.now - breaker.duration - 30 }
64
+
65
+ it 'calls the block' do
66
+ # This is the same as being half open, see below for further tests.
67
+ expect(breaker.call(arg)).to eq arg
68
+ end
69
+ end
70
+ end
71
+
72
+ context 'when the circuit is half open' do
73
+ before do
74
+ # For the circuit to be tripped in the first place, the failure count
75
+ # must have reached the failure threshold.
76
+ breaker.failure_count = breaker.failure_threshold
77
+ breaker.half_open!
78
+ end
79
+
80
+ it 'calls the block' do
81
+ expect(breaker.call(arg)).to eq arg
82
+ end
83
+
84
+ context 'and the call succeeds' do
85
+ before { breaker.failure_count = 3 }
86
+
87
+ it { is_expected.to change { breaker.closed? }.to(true) }
88
+ it { is_expected.to change { breaker.failure_count }.to(0) }
89
+ end
90
+
91
+ context 'and the call fails' do
92
+ let(:block) { -> { raise 'some error' } }
93
+
94
+ it { is_expected.to change { breaker.open? }.to(true) }
95
+ it { is_expected.to change { breaker.last_failed } }
96
+ end
97
+ end
98
+ end
99
+
100
+ # #half_open?, #half_open!, #closed?, and #closed! are all exactly the same
101
+ # as #open? and #open!, so we're just going to test the open methods.
102
+
103
+ describe '#open!' do
104
+ it 'opens the circuit' do
105
+ breaker.open!
106
+ expect(breaker).to be_open
107
+ end
108
+ end
109
+
110
+ describe '#open?' do
111
+ subject { breaker.open? }
112
+
113
+ context 'when open' do
114
+ before { breaker.open! }
115
+ it { is_expected.to be_truthy }
116
+ end
117
+
118
+ context 'when not open' do
119
+ before { breaker.closed! }
120
+ it { is_expected.to be_falsey }
121
+ end
122
+ end
123
+ end
124
+ end
125
+
126
+ class MockCache
127
+ attr_accessor :stored_data
128
+
129
+ def initialize
130
+ @stored_data = {}
131
+ end
132
+
133
+ def write(key, val)
134
+ @stored_data[key] = val
135
+ end
136
+
137
+ def fetch(key)
138
+ @stored_data[key]
139
+ end
140
+ end
@@ -0,0 +1,17 @@
1
+ require 'pry'
2
+
3
+ lib_dir = File.expand_path('../../lib/', __FILE__)
4
+ Dir["#{lib_dir}/**/*.rb"].each { |file| require file }
5
+
6
+ RSpec.configure do |config|
7
+ if config.files_to_run.one?
8
+ config.default_formatter = 'doc'
9
+ end
10
+
11
+ config.order = :random
12
+ Kernel.srand config.seed
13
+
14
+ config.mock_with :rspec do |mocks|
15
+ mocks.verify_partial_doubles = true
16
+ end
17
+ end
metadata ADDED
@@ -0,0 +1,118 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: circuit_breakage
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - John Hyland
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-09-04 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.6'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.6'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: pry
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description: Provides a circuit breaker pattern with configurable error tolerance,
70
+ timeout, breakage duration, and state storage.
71
+ email:
72
+ - john@djspinmonkey.com
73
+ executables: []
74
+ extensions: []
75
+ extra_rdoc_files: []
76
+ files:
77
+ - ".gitignore"
78
+ - ".rspec"
79
+ - Gemfile
80
+ - LICENSE.txt
81
+ - README.md
82
+ - Rakefile
83
+ - circuit_breakage.gemspec
84
+ - lib/circuit_breakage.rb
85
+ - lib/circuit_breakage/breaker.rb
86
+ - lib/circuit_breakage/caching_breaker.rb
87
+ - lib/circuit_breakage/version.rb
88
+ - spec/breaker_spec.rb
89
+ - spec/caching_breaker_spec.rb
90
+ - spec/spec_helper.rb
91
+ homepage: https://source.datanerd.us/jhyland/circuit_breakage
92
+ licenses:
93
+ - New Relic
94
+ metadata: {}
95
+ post_install_message:
96
+ rdoc_options: []
97
+ require_paths:
98
+ - lib
99
+ required_ruby_version: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ required_rubygems_version: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - ">="
107
+ - !ruby/object:Gem::Version
108
+ version: '0'
109
+ requirements: []
110
+ rubyforge_project:
111
+ rubygems_version: 2.2.2
112
+ signing_key:
113
+ specification_version: 4
114
+ summary: Provides a simple circuit breaker pattern.
115
+ test_files:
116
+ - spec/breaker_spec.rb
117
+ - spec/caching_breaker_spec.rb
118
+ - spec/spec_helper.rb