breaker 0.1.0

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: e96a4a527f8764f01191e363daf25d59bcfc44a1
4
+ data.tar.gz: 9707df9e63bbdce0166b471beab29456f67c5c6b
5
+ SHA512:
6
+ metadata.gz: 43cb66bf520aa9d6abd763b274b195a2a64387215c7c31799430f576a742f602dc8ac466edd3b88bcb948d37ade1b5a0f9ac98559ae2673afa9dc1358f4e1dbc
7
+ data.tar.gz: 4436478a597986c372c032ac72bf0dea6977a2b34b27cfc294113746f6bb6ef4bdf1d454c47d90a794364c57d1421c172b25a218a1e1ae86345bc4a3ebdc0715
data/.gitignore ADDED
@@ -0,0 +1,17 @@
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
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in breaker.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 ahawkins
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,96 @@
1
+ # Breaker
2
+
3
+ Circuit Breakers for well designed applications.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'breaker'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install breaker
18
+
19
+ ## Circuit Breaker Pattern
20
+
21
+ The circuit breaker pattern is described in the (wonderful) book
22
+ [Release It](http://pragprog.com/book/mnee/release-it) by Michael T.
23
+ Nygard. A circuit breaker in programming terms is modeled after a
24
+ circuit breaker in the real world. A circuit breaker protects the
25
+ larger system from failures in other systems. They are especially
26
+ useful for protecting an application from finnicky remote systems.
27
+
28
+ The circuit is a state machine. It has three states: open, closed, and
29
+ half-open. The circuit starts off in `open` state--normal operation.
30
+ If the operation failures N times (the failure threshold) the circuit
31
+ moves to `closed`. Calls in the `closed` state will immediate fail and
32
+ raise an exception. After a specified time period has passed (retry
33
+ timeout) the circuit moves into `half-open`. Calls happen normally. If
34
+ a call fails the state moves to `open`. If the call suceeds it moves
35
+ to `open`. All calls are capped with a timeout. If a timeout occurs
36
+ that counts as a failure.
37
+
38
+ ## Usage
39
+
40
+ Most interaction should go through `Breaker.circuit`. This is a
41
+ factory method that creates `Breaker::Circuit` objects. It requires
42
+ one argument: the name. It also takes an `options` hash for
43
+ customizing the circuit. Thirdly it takes a block to run inside the
44
+ circuit. Here are some examples:
45
+
46
+ ```ruby
47
+ # Simplest example: protect some code with a circuit breaker
48
+ Breaker.circuit 'twitter' do
49
+ Tweet.post 'Oh Hai'
50
+ end
51
+ ```
52
+
53
+ `Breaker.circuit` is an upsert opertion. It will create or update an
54
+ existing circuit. Pass the options hash to customize the circuit's
55
+ behavior.
56
+
57
+ ```ruby
58
+ circuit = Breaker.circuit 'twitter', timeout: 5
59
+ circuit.run do
60
+ Tweet.post 'Oh Hai'
61
+ end
62
+ ```
63
+
64
+ `Breaker.circuit` returns `Breaker::Circuit` instances which can be
65
+ saved for later. There use depends no how persistence works.
66
+
67
+ ## Persistence
68
+
69
+ Circuit breakers are only really useful in a large system (perhaps
70
+ distributed). Some information must be shared acrosss subsystems. Say
71
+ there are 5 different services in the system. Each is
72
+ talking to 2 different systems protected by circuit breakers. Either
73
+ of the systems go down. It's natural that the failure should propogate
74
+ through the system so that each service knows the shared ones are
75
+ down. This is where state and persistence come into play.
76
+
77
+ The breaker gems bundles a simple in memory repository. This is
78
+ process specific. If you need to share state across multiple processes
79
+ then you must write your own repository.
80
+
81
+ The repository manages fueses (in the eletrical sense). A fuse
82
+ maintains state. The repository must implement one method: `upsert`
83
+ which creates or updates a fuse given by name. The repistory can
84
+ return some sort of persistent fuse where writer methods write to
85
+ persistent storage.
86
+
87
+ Refer to `lib/breaker/in_memory_repo.rb` for an example. The class is
88
+ very simple.
89
+
90
+ ## Contributing
91
+
92
+ 1. Fork it
93
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
94
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
95
+ 4. Push to the branch (`git push origin my-new-feature`)
96
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ require 'rake/testtask'
4
+
5
+ Rake::TestTask.new(:test) do |t|
6
+ t.test_files = Rake::FileList['test/**/*_test.rb']
7
+ end
8
+
9
+ task default: :test
data/breaker.gemspec ADDED
@@ -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 'breaker/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "breaker"
8
+ spec.version = Breaker::VERSION
9
+ spec.authors = ["ahawkins"]
10
+ spec.email = ["adam@hawkins.io"]
11
+ spec.description = %q{Circuit breaker pattern for well designed Ruby applications }
12
+ spec.summary = %q{}
13
+ spec.homepage = "https://github.com/ahawkins/breaker"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
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.3"
22
+ spec.add_development_dependency "rake"
23
+ end
data/lib/breaker.rb ADDED
@@ -0,0 +1,133 @@
1
+ require "breaker/version"
2
+ require 'timeout'
3
+
4
+ module Breaker
5
+ CircuitOpenError = Class.new RuntimeError
6
+
7
+ class << self
8
+ def circuit(name, options = {})
9
+ fuse = repo.upsert options.merge(name: name)
10
+
11
+ circuit = Circuit.new fuse
12
+
13
+ if block_given?
14
+ circuit.run do
15
+ yield
16
+ end
17
+ end
18
+
19
+ circuit
20
+ end
21
+
22
+ def closed?(name)
23
+ circuit(name).closed?
24
+ end
25
+ alias up? closed?
26
+
27
+ def open?(name)
28
+ circuit(name).open?
29
+ end
30
+ alias down? open?
31
+
32
+ def repo
33
+ @repo
34
+ end
35
+
36
+ def repo=(repo)
37
+ @repo = repo
38
+ end
39
+ end
40
+
41
+ class Circuit
42
+ attr_accessor :fuse
43
+
44
+ def initialize(fuse)
45
+ @fuse = fuse
46
+ end
47
+
48
+ def name
49
+ fuse.name
50
+ end
51
+
52
+ def open(clock = Time.now)
53
+ fuse.failure_count = 1
54
+ fuse.state = :open
55
+ fuse.retry_threshold = clock + retry_timeout
56
+ end
57
+
58
+ def close
59
+ fuse.failure_count = 0
60
+ fuse.state = :closed
61
+ fuse.retry_threshold = nil
62
+ end
63
+
64
+ def ==(other)
65
+ other.instance_of?(self.class) && fuse == other.fuse
66
+ end
67
+
68
+ def open?
69
+ fuse.state == :open
70
+ end
71
+ alias down? open?
72
+
73
+ def closed?
74
+ fuse.state == :closed
75
+ end
76
+ alias up? closed?
77
+
78
+ def retry_timeout
79
+ fuse.retry_timeout
80
+ end
81
+
82
+ def failure_count
83
+ fuse.failure_count
84
+ end
85
+
86
+ def failure_threshold
87
+ fuse.failure_threshold
88
+ end
89
+
90
+ def timeout
91
+ fuse.timeout
92
+ end
93
+
94
+ def run(clock = Time.now)
95
+ if closed? || half_open?(clock)
96
+ begin
97
+ result = Timeout.timeout timeout do
98
+ yield
99
+ end
100
+
101
+ if half_open?(clock)
102
+ close
103
+ end
104
+
105
+ result
106
+ rescue => ex
107
+ fuse.failure_count = fuse.failure_count + 1
108
+ fuse.retry_threshold = clock + retry_timeout
109
+
110
+ open clock
111
+
112
+ raise ex
113
+ end
114
+ else
115
+ raise Breaker::CircuitOpenError, "Cannot run code while #{name} is open!"
116
+ end
117
+ end
118
+
119
+ private
120
+ def tripped?
121
+ fuse.failure_count != 0
122
+ end
123
+
124
+ def half_open?(clock)
125
+ tripped? && clock >= fuse.retry_threshold
126
+ end
127
+ end
128
+ end
129
+
130
+ require_relative 'breaker/in_memory_repo'
131
+ require_relative 'breaker/test_cases'
132
+
133
+ Breaker.repo = Breaker::InMemoryRepo.new
@@ -0,0 +1,69 @@
1
+ module Breaker
2
+ class InMemoryRepo
3
+ Fuse = Struct.new :name, :state, :failure_threshold, :retry_timeout, :timeout, :failure_count, :retry_threshold do
4
+ def initialize(*args)
5
+ super
6
+
7
+ self.failure_threshold ||= 10
8
+ self.retry_timeout ||= 60
9
+ self.timeout ||= 5
10
+
11
+ self.state ||= :closed
12
+ self.failure_count ||= 0
13
+ end
14
+
15
+ def ==(other)
16
+ other.instance_of?(self.class) && name == other.name
17
+ end
18
+ end
19
+
20
+ attr_reader :store
21
+
22
+ def initialize
23
+ @store = []
24
+ end
25
+
26
+ def upsert(attributes)
27
+ existing = named attributes.fetch(:name)
28
+ if existing
29
+ update existing, attributes
30
+ else
31
+ create attributes
32
+ end
33
+ end
34
+
35
+ def count
36
+ store.length
37
+ end
38
+
39
+ def first
40
+ store.first
41
+ end
42
+
43
+ def named(name)
44
+ store.find { |fuse| fuse.name == name }
45
+ end
46
+
47
+ def create(attributes)
48
+ fuse = Fuse.new
49
+
50
+ attributes.each_pair do |key, value|
51
+ fuse.send "#{key}=", value
52
+ end
53
+
54
+ store << fuse
55
+
56
+ fuse
57
+ end
58
+
59
+ def update(existing, attributes)
60
+ existing
61
+
62
+ attributes.each_pair do |key, value|
63
+ existing.send "#{key}=", value
64
+ end
65
+
66
+ existing
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,159 @@
1
+ module Breaker
2
+ module TestCases
3
+ DummyError = Class.new RuntimeError
4
+
5
+ def repo
6
+ flunk "Test must define a repo to use"
7
+ end
8
+
9
+ def setup
10
+ Breaker.repo = repo
11
+ end
12
+
13
+ def test_new_fuses_start_off_clean
14
+ circuit = Breaker.circuit 'test'
15
+
16
+ assert circuit.closed?, "New circuits should be closed"
17
+ assert_equal 0, circuit.failure_count
18
+ end
19
+
20
+ def test_goes_into_open_state_when_failure_threshold_reached
21
+ circuit = Breaker.circuit 'test', failure_threshold: 1, retry_timeout: 30
22
+
23
+ assert circuit.closed?
24
+
25
+ assert_raises DummyError do
26
+ circuit.run do
27
+ raise DummyError
28
+ end
29
+ end
30
+
31
+ assert circuit.open?
32
+ assert_raises Breaker::CircuitOpenError do
33
+ circuit.run do
34
+ assert false, "Block should not run in this state"
35
+ end
36
+ end
37
+ end
38
+
39
+ def test_success_in_half_open_state_moves_circuit_into_closed
40
+ clock = Time.now
41
+ circuit = Breaker.circuit 'test', failure_threshold: 2, retry_timeout: 15
42
+
43
+ circuit.open clock
44
+ assert circuit.open?
45
+
46
+ assert_raises Breaker::CircuitOpenError do
47
+ circuit.run clock do
48
+ # nothing
49
+ end
50
+ end
51
+
52
+ circuit.run clock + circuit.retry_timeout do
53
+ # do nothing, this works and flips the circuit back closed
54
+ end
55
+
56
+ assert circuit.closed?
57
+ end
58
+
59
+ def test_failures_in_half_open_state_push_retry_timeout_back
60
+ clock = Time.now
61
+ circuit = Breaker.circuit 'test', failure_threshold: 2, retry_timeout: 15
62
+
63
+ circuit.open clock
64
+ assert circuit.open?
65
+
66
+ assert_raises DummyError do
67
+ circuit.run clock + circuit.retry_timeout do
68
+ raise DummyError
69
+ end
70
+ end
71
+
72
+ assert_raises Breaker::CircuitOpenError do
73
+ circuit.run clock + circuit.retry_timeout do
74
+ assert false, "Block should not be run while in this state"
75
+ end
76
+ end
77
+
78
+ assert_raises DummyError do
79
+ circuit.run clock + circuit.retry_timeout * 2 do
80
+ raise DummyError
81
+ end
82
+ end
83
+ end
84
+
85
+ def test_counts_timeouts_as_trips
86
+ circuit = Breaker.circuit 'test', retry_timeout: 15, timeout: 0.01
87
+ assert circuit.closed?
88
+
89
+ assert_raises TimeoutError do
90
+ circuit.run do
91
+ sleep circuit.timeout * 2
92
+ end
93
+ end
94
+ end
95
+
96
+ def test_circuit_factory_persists_fuses
97
+ circuit_a = Breaker.circuit 'test'
98
+ circuit_b = Breaker.circuit 'test'
99
+
100
+ assert_equal circuit_a, circuit_b, "Multiple calls to `circuit` should return the same circuit"
101
+
102
+ assert_equal 1, Breaker.repo.count
103
+ fuse = Breaker.repo.first
104
+
105
+ assert_equal 'test', fuse.name
106
+ end
107
+
108
+ def test_circuit_factory_creates_new_fuses_with_sensible_defaults
109
+ circuit = Breaker.circuit 'test'
110
+
111
+ assert_equal 1, Breaker.repo.count
112
+ fuse = Breaker.repo.first
113
+
114
+ assert_equal 10, fuse.failure_threshold, "Failure Theshold should have a default"
115
+ assert_equal 60, fuse.retry_timeout, "Retry timeout should have a default"
116
+ assert_equal 5, fuse.timeout, "Timeout should have a default"
117
+ end
118
+
119
+ def test_circuit_factory_updates_existing_fuses
120
+ Breaker.circuit 'test'
121
+ assert_equal 1, Breaker.repo.count
122
+
123
+ Breaker.circuit 'test', failure_threshold: 1,
124
+ retry_timeout: 2, timeout: 3
125
+
126
+ assert_equal 1, Breaker.repo.count
127
+ fuse = Breaker.repo.first
128
+
129
+ assert_equal 1, fuse.failure_threshold
130
+ assert_equal 2, fuse.retry_timeout
131
+ assert_equal 3, fuse.timeout
132
+ end
133
+
134
+ def test_circuit_breaker_factory_can_run_code_through_the_circuit
135
+ assert_raises DummyError do
136
+ Breaker.circuit 'test' do
137
+ raise DummyError
138
+ end
139
+ end
140
+ end
141
+
142
+ def test_breaker_query_methods
143
+ circuit = Breaker.circuit 'test'
144
+ circuit.close
145
+
146
+ assert Breaker.closed?('test')
147
+ assert Breaker.up?('test')
148
+ refute Breaker.open?('test')
149
+ refute Breaker.down?('test')
150
+
151
+ circuit.open
152
+
153
+ assert Breaker.open?('test')
154
+ assert Breaker.down?('test')
155
+ refute Breaker.closed?('test')
156
+ refute Breaker.up?('test')
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,3 @@
1
+ module Breaker
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,16 @@
1
+ require_relative 'test_helper'
2
+
3
+ class AcceptanceTest < MiniTest::Unit::TestCase
4
+ include Breaker::TestCases
5
+
6
+ InMemoryFuse = Struct.new :state, :failure_count, :retry_threshold,
7
+ :failure_threshold, :retry_timeout, :timeout
8
+
9
+ attr_reader :fuse, :repo
10
+
11
+ def setup
12
+ @repo = Breaker::InMemoryRepo.new
13
+ @fuse = InMemoryFuse.new :closed, 0, nil, 3, 15, 10
14
+ super
15
+ end
16
+ end
@@ -0,0 +1,5 @@
1
+ require 'bundler/setup'
2
+
3
+ require 'minitest/autorun'
4
+
5
+ require 'breaker'
metadata ADDED
@@ -0,0 +1,86 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: breaker
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - ahawkins
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-01-14 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.3'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.3'
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
+ description: 'Circuit breaker pattern for well designed Ruby applications '
42
+ email:
43
+ - adam@hawkins.io
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - ".gitignore"
49
+ - Gemfile
50
+ - LICENSE.txt
51
+ - README.md
52
+ - Rakefile
53
+ - breaker.gemspec
54
+ - lib/breaker.rb
55
+ - lib/breaker/in_memory_repo.rb
56
+ - lib/breaker/test_cases.rb
57
+ - lib/breaker/version.rb
58
+ - test/acceptance_test.rb
59
+ - test/test_helper.rb
60
+ homepage: https://github.com/ahawkins/breaker
61
+ licenses:
62
+ - MIT
63
+ metadata: {}
64
+ post_install_message:
65
+ rdoc_options: []
66
+ require_paths:
67
+ - lib
68
+ required_ruby_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: '0'
73
+ required_rubygems_version: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ requirements: []
79
+ rubyforge_project:
80
+ rubygems_version: 2.2.0
81
+ signing_key:
82
+ specification_version: 4
83
+ summary: ''
84
+ test_files:
85
+ - test/acceptance_test.rb
86
+ - test/test_helper.rb