wsargent-circuit_breaker 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt ADDED
@@ -0,0 +1,3 @@
1
+ === 1.0.0 / 2009-07-13
2
+
3
+ * Initial release
data/Manifest.txt ADDED
@@ -0,0 +1,11 @@
1
+ History.txt
2
+ Manifest.txt
3
+ README.txt
4
+ Rakefile
5
+ circuit_breaker.gemspec
6
+ lib/circuit_breaker.rb
7
+ lib/circuit_breaker/circuit_state.rb
8
+ lib/circuit_breaker/circuit_handler.rb
9
+ lib/circuit_breaker/circuit_broken_exception.rb
10
+ spec/unit_spec_helper.rb
11
+ spec/unit/circuit_breaker_spec.rb
data/README.txt ADDED
@@ -0,0 +1,234 @@
1
+ = circuit_breaker
2
+
3
+ * http://github.com/wsargent/circuit_breaker
4
+ * http://rdoc.info/projects/wsargent/circuit_breaker
5
+ * Will Sargent <will.sargent@gmail.com>
6
+ * Copyright 2009 Will Sargent
7
+
8
+ == DESCRIPTION:
9
+
10
+ CircuitBreaker is a relatively simple Ruby mixin that will wrap
11
+ a call to a given service in a circuit breaker pattern.
12
+
13
+ The circuit starts off "closed" meaning that all calls will go through.
14
+ However, consecutive failures are recorded and after a threshold is reached,
15
+ the circuit will "trip", setting the circuit into an "open" state.
16
+
17
+ In an "open" state, every call to the service will fail by raising
18
+ CircuitBrokenException.
19
+
20
+ The circuit will remain in an "open" state until the failure timeout has
21
+ elapsed.
22
+
23
+ After the failure_timeout has elapsed, the circuit will go into
24
+ a "half open" state and the call will go through. A failure will
25
+ immediately pop the circuit open again, and a success will close the
26
+ circuit and reset the failure count.
27
+
28
+ require 'circuit_breaker'
29
+ class TestService
30
+
31
+ include CircuitBreaker
32
+
33
+ def call_remote_service() ...
34
+
35
+ circuit_method :call_remote_service
36
+
37
+ # Optional
38
+ circuit_handler do |handler|
39
+ handler.logger = Logger.new(STDOUT)
40
+ handler.failure_threshold = 5
41
+ handler.failure_timeout = 5
42
+ end
43
+
44
+ # Optional
45
+ circuit_handler_class MyCustomCircuitHandler
46
+ end
47
+
48
+ == FEATURES/PROBLEMS:
49
+
50
+ * Can run out of the box with minimal dependencies and a couple of lines of code.
51
+ * Easy to extend: add your own circuit breakers or states or extend the existing ones.
52
+ * Does not currently handle static class methods.
53
+
54
+ == SYNOPSIS:
55
+
56
+ An implementation of Michael Nygard's Circuit Breaker pattern.
57
+
58
+ == REQUIREMENTS:
59
+
60
+ circuit_breaker has a dependency on AASM @ http://github.com/rubyist/aasm/tree/master
61
+
62
+ == INSTALL:
63
+
64
+ * gem sources -a http://gems.github.com
65
+ * gem install rubyist-aasm
66
+ * gem install wsargent_circuit-breaker
67
+
68
+ == LICENSE:
69
+
70
+ GNU LESSER GENERAL PUBLIC LICENSE
71
+ Version 3, 29 June 2007
72
+
73
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
74
+ Everyone is permitted to copy and distribute verbatim copies
75
+ of this license document, but changing it is not allowed.
76
+
77
+
78
+ This version of the GNU Lesser General Public License incorporates
79
+ the terms and conditions of version 3 of the GNU General Public
80
+ License, supplemented by the additional permissions listed below.
81
+
82
+ 0. Additional Definitions.
83
+
84
+ As used herein, "this License" refers to version 3 of the GNU Lesser
85
+ General Public License, and the "GNU GPL" refers to version 3 of the GNU
86
+ General Public License.
87
+
88
+ "The Library" refers to a covered work governed by this License,
89
+ other than an Application or a Combined Work as defined below.
90
+
91
+ An "Application" is any work that makes use of an interface provided
92
+ by the Library, but which is not otherwise based on the Library.
93
+ Defining a subclass of a class defined by the Library is deemed a mode
94
+ of using an interface provided by the Library.
95
+
96
+ A "Combined Work" is a work produced by combining or linking an
97
+ Application with the Library. The particular version of the Library
98
+ with which the Combined Work was made is also called the "Linked
99
+ Version".
100
+
101
+ The "Minimal Corresponding Source" for a Combined Work means the
102
+ Corresponding Source for the Combined Work, excluding any source code
103
+ for portions of the Combined Work that, considered in isolation, are
104
+ based on the Application, and not on the Linked Version.
105
+
106
+ The "Corresponding Application Code" for a Combined Work means the
107
+ object code and/or source code for the Application, including any data
108
+ and utility programs needed for reproducing the Combined Work from the
109
+ Application, but excluding the System Libraries of the Combined Work.
110
+
111
+ 1. Exception to Section 3 of the GNU GPL.
112
+
113
+ You may convey a covered work under sections 3 and 4 of this License
114
+ without being bound by section 3 of the GNU GPL.
115
+
116
+ 2. Conveying Modified Versions.
117
+
118
+ If you modify a copy of the Library, and, in your modifications, a
119
+ facility refers to a function or data to be supplied by an Application
120
+ that uses the facility (other than as an argument passed when the
121
+ facility is invoked), then you may convey a copy of the modified
122
+ version:
123
+
124
+ a) under this License, provided that you make a good faith effort to
125
+ ensure that, in the event an Application does not supply the
126
+ function or data, the facility still operates, and performs
127
+ whatever part of its purpose remains meaningful, or
128
+
129
+ b) under the GNU GPL, with none of the additional permissions of
130
+ this License applicable to that copy.
131
+
132
+ 3. Object Code Incorporating Material from Library Header Files.
133
+
134
+ The object code form of an Application may incorporate material from
135
+ a header file that is part of the Library. You may convey such object
136
+ code under terms of your choice, provided that, if the incorporated
137
+ material is not limited to numerical parameters, data structure
138
+ layouts and accessors, or small macros, inline functions and templates
139
+ (ten or fewer lines in length), you do both of the following:
140
+
141
+ a) Give prominent notice with each copy of the object code that the
142
+ Library is used in it and that the Library and its use are
143
+ covered by this License.
144
+
145
+ b) Accompany the object code with a copy of the GNU GPL and this license
146
+ document.
147
+
148
+ 4. Combined Works.
149
+
150
+ You may convey a Combined Work under terms of your choice that,
151
+ taken together, effectively do not restrict modification of the
152
+ portions of the Library contained in the Combined Work and reverse
153
+ engineering for debugging such modifications, if you also do each of
154
+ the following:
155
+
156
+ a) Give prominent notice with each copy of the Combined Work that
157
+ the Library is used in it and that the Library and its use are
158
+ covered by this License.
159
+
160
+ b) Accompany the Combined Work with a copy of the GNU GPL and this license
161
+ document.
162
+
163
+ c) For a Combined Work that displays copyright notices during
164
+ execution, include the copyright notice for the Library among
165
+ these notices, as well as a reference directing the user to the
166
+ copies of the GNU GPL and this license document.
167
+
168
+ d) Do one of the following:
169
+
170
+ 0) Convey the Minimal Corresponding Source under the terms of this
171
+ License, and the Corresponding Application Code in a form
172
+ suitable for, and under terms that permit, the user to
173
+ recombine or relink the Application with a modified version of
174
+ the Linked Version to produce a modified Combined Work, in the
175
+ manner specified by section 6 of the GNU GPL for conveying
176
+ Corresponding Source.
177
+
178
+ 1) Use a suitable shared library mechanism for linking with the
179
+ Library. A suitable mechanism is one that (a) uses at run time
180
+ a copy of the Library already present on the user's computer
181
+ system, and (b) will operate properly with a modified version
182
+ of the Library that is interface-compatible with the Linked
183
+ Version.
184
+
185
+ e) Provide Installation Information, but only if you would otherwise
186
+ be required to provide such information under section 6 of the
187
+ GNU GPL, and only to the extent that such information is
188
+ necessary to install and execute a modified version of the
189
+ Combined Work produced by recombining or relinking the
190
+ Application with a modified version of the Linked Version. (If
191
+ you use option 4d0, the Installation Information must accompany
192
+ the Minimal Corresponding Source and Corresponding Application
193
+ Code. If you use option 4d1, you must provide the Installation
194
+ Information in the manner specified by section 6 of the GNU GPL
195
+ for conveying Corresponding Source.)
196
+
197
+ 5. Combined Libraries.
198
+
199
+ You may place library facilities that are a work based on the
200
+ Library side by side in a single library together with other library
201
+ facilities that are not Applications and are not covered by this
202
+ License, and convey such a combined library under terms of your
203
+ choice, if you do both of the following:
204
+
205
+ a) Accompany the combined library with a copy of the same work based
206
+ on the Library, uncombined with any other library facilities,
207
+ conveyed under the terms of this License.
208
+
209
+ b) Give prominent notice with the combined library that part of it
210
+ is a work based on the Library, and explaining where to find the
211
+ accompanying uncombined form of the same work.
212
+
213
+ 6. Revised Versions of the GNU Lesser General Public License.
214
+
215
+ The Free Software Foundation may publish revised and/or new versions
216
+ of the GNU Lesser General Public License from time to time. Such new
217
+ versions will be similar in spirit to the present version, but may
218
+ differ in detail to address new problems or concerns.
219
+
220
+ Each version is given a distinguishing version number. If the
221
+ Library as you received it specifies that a certain numbered version
222
+ of the GNU Lesser General Public License "or any later version"
223
+ applies to it, you have the option of following the terms and
224
+ conditions either of that published version or of any later version
225
+ published by the Free Software Foundation. If the Library as you
226
+ received it does not specify a version number of the GNU Lesser
227
+ General Public License, you may choose any version of the GNU Lesser
228
+ General Public License ever published by the Free Software Foundation.
229
+
230
+ If the Library as you received it specifies that a proxy can decide
231
+ whether future versions of the GNU Lesser General Public License shall
232
+ apply, that proxy's public statement of acceptance of any version is
233
+ permanent authorization for you to choose that version for the
234
+ Library.
data/Rakefile ADDED
@@ -0,0 +1,26 @@
1
+ # -*- ruby -*-
2
+ $:.unshift(File.dirname(__FILE__) + "/lib")
3
+
4
+ require 'rubygems'
5
+ require 'hoe'
6
+ require 'circuit_breaker'
7
+
8
+ hoe = Hoe.spec 'circuit_breaker' do |p|
9
+ self.rubyforge_name = 'will_sargent'
10
+ developer('Will Sargent', 'will.sargent@gmail.com')
11
+
12
+ p.remote_rdoc_dir = '' # Release to root only one project
13
+
14
+ p.extra_deps << [ 'rubyist-aasm' ]
15
+ p.extra_dev_deps << [ 'rspec' ]
16
+ File.open(File.join(File.dirname(__FILE__), 'VERSION'), 'w') do |file|
17
+ file.puts CircuitBreaker::VERSION
18
+ end
19
+ end
20
+
21
+ begin
22
+ require 'jeweler'
23
+ Jeweler::Tasks.new(hoe.spec)
24
+ rescue LoadError
25
+ puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
26
+ end
@@ -0,0 +1,76 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{circuit_breaker}
5
+ s.version = "1.0.0"
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["Will Sargent"]
9
+ s.date = %q{2009-07-13}
10
+ s.description = %q{CircuitBreaker is a relatively simple Ruby mixin that will wrap
11
+ a call to a given service in a circuit breaker pattern.
12
+
13
+ The circuit starts off "closed" meaning that all calls will go through.
14
+ However, consecutive failures are recorded and after a threshold is reached,
15
+ the circuit will "trip", setting the circuit into an "open" state.
16
+
17
+ In an "open" state, every call to the service will fail by raising
18
+ CircuitBrokenException.
19
+
20
+ The circuit will remain in an "open" state until the failure timeout has
21
+ elapsed.
22
+
23
+ After the failure_timeout has elapsed, the circuit will go into
24
+ a "half open" state and the call will go through. A failure will
25
+ immediately pop the circuit open again, and a success will close the
26
+ circuit and reset the failure count.
27
+
28
+ require 'circuit_breaker'
29
+ class TestService
30
+
31
+ include CircuitBreaker
32
+
33
+ def call_remote_service() ...
34
+
35
+ circuit_method :call_remote_service
36
+
37
+ # Optional
38
+ circuit_handler do |handler|
39
+ handler.logger = Logger.new(STDOUT)
40
+ handler.failure_threshold = 5
41
+ handler.failure_timeout = 5
42
+ end
43
+
44
+ # Optional
45
+ circuit_handler_class MyCustomCircuitHandler
46
+ end}
47
+ s.email = ["will.sargent@gmail.com"]
48
+ s.extra_rdoc_files = ["History.txt", "Manifest.txt", "README.txt"]
49
+ s.files = ["History.txt", "Manifest.txt", "README.txt", "Rakefile", "circuit_breaker.gemspec", "lib/circuit_breaker.rb", "lib/circuit_breaker/circuit_state.rb", "lib/circuit_breaker/circuit_handler.rb", "lib/circuit_breaker/circuit_broken_exception.rb", "spec/unit_spec_helper.rb", "spec/unit/circuit_breaker_spec.rb"]
50
+ s.homepage = %q{http://github.com/wsargent/circuit_breaker}
51
+ s.rdoc_options = ["--main", "README.txt", "--charset=UTF-8"]
52
+ s.require_paths = ["lib"]
53
+ s.rubyforge_project = %q{will_sargent}
54
+ s.rubygems_version = %q{1.3.4}
55
+ s.summary = %q{CircuitBreaker is a relatively simple Ruby mixin that will wrap a call to a given service in a circuit breaker pattern}
56
+ s.test_files = ["spec/unit/circuit_breaker_spec.rb", "spec/unit_spec_helper.rb"]
57
+
58
+ if s.respond_to? :specification_version then
59
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
60
+ s.specification_version = 3
61
+
62
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
63
+ s.add_runtime_dependency(%q<rubyist-aasm>, [">= 0"])
64
+ s.add_development_dependency(%q<rspec>, [">= 1.2.7"])
65
+ s.add_development_dependency(%q<hoe>, [">= 2.3.1"])
66
+ else
67
+ s.add_dependency(%q<rubyist-aasm>, [">= 0"])
68
+ s.add_dependency(%q<rspec>, [">= 1.2.7"])
69
+ s.add_dependency(%q<hoe>, [">= 2.3.1"])
70
+ end
71
+ else
72
+ s.add_dependency(%q<rubyist-aasm>, [">= 0"])
73
+ s.add_dependency(%q<rspec>, [">= 1.2.7"])
74
+ s.add_dependency(%q<hoe>, [">= 2.3.1"])
75
+ end
76
+ end
@@ -0,0 +1,103 @@
1
+ #
2
+ # CircuitBreaker is a relatively simple Ruby mixin that will wrap
3
+ # a call to a given service in a circuit breaker pattern.
4
+ #
5
+ # The circuit starts off "closed" meaning that all calls will go through.
6
+ # However, consecutive failures are recorded and after a threshold is reached,
7
+ # the circuit will "trip", setting the circuit into an "open" state.
8
+ #
9
+ # In an "open" state, every call to the service will fail by raising
10
+ # CircuitBrokenException.
11
+ #
12
+ # The circuit will remain in an "open" state until the failure timeout has
13
+ # elapsed.
14
+ #
15
+ # After the failure_timeout has elapsed, the circuit will go into
16
+ # a "half open" state and the call will go through. A failure will
17
+ # immediately pop the circuit open again, and a success will close the
18
+ # circuit and reset the failure count.
19
+ #
20
+ # require 'circuit_breaker'
21
+ # class TestService
22
+ #
23
+ # include CircuitBreaker
24
+ #
25
+ # def call_remote_service() ...
26
+ #
27
+ # circuit_method :call_remote_service
28
+ #
29
+ # # Optional
30
+ # circuit_handler do |handler|
31
+ # handler.logger = Logger.new(STDOUT)
32
+ # handler.failure_threshold = 5
33
+ # handler.failure_timeout = 5
34
+ # end
35
+ #
36
+ # # Optional
37
+ # circuit_handler_class MyCustomCircuitHandler
38
+ #
39
+ # end
40
+ #
41
+ # Copyright 2009 Will Sargent
42
+ # Author: Will Sargent <will.sargent@gmail.com>
43
+ # Many thanks to Devin Mullins
44
+ #
45
+ module CircuitBreaker
46
+ VERSION = '1.0.0'
47
+
48
+ #
49
+ # Extends the included class with CircuitBreaker
50
+ #
51
+ def self.included(klass)
52
+ klass.extend ::CircuitBreaker::ClassMethods
53
+ end
54
+
55
+ #
56
+ # Returns the current circuit state. This is defined on the instance, so
57
+ # you can have several instances of the same class with different states.
58
+ #
59
+ def circuit_state
60
+ @circuit_state ||= self.class.circuit_handler.new_circuit_state
61
+ end
62
+
63
+ module ClassMethods
64
+
65
+ #
66
+ # Takes a splat of method names, and wraps them with the circuit_handler.
67
+ #
68
+ def circuit_method(*methods)
69
+ circuit_handler = self.circuit_handler
70
+
71
+ methods.each do |meth|
72
+ m = instance_method meth
73
+ define_method meth do |*args|
74
+ circuit_handler.handle self.circuit_state, m.bind(self), *args
75
+ end
76
+ end
77
+ end
78
+
79
+ #
80
+ # Returns circuit_handler. Yields the instance back when passed a block.
81
+ #
82
+ def circuit_handler(&block)
83
+ @circuit_handler ||= circuit_handler_class.new
84
+
85
+ yield @circuit_handler if block_given?
86
+
87
+ return @circuit_handler
88
+ end
89
+
90
+ #
91
+ # Allows you to define a custom circuit_handler instead of CircuitBreaker::CircuitHandler
92
+ #
93
+ def circuit_handler_class(klass = nil)
94
+ @circuit_handler_class ||= (klass || CircuitBreaker::CircuitHandler)
95
+ end
96
+
97
+ end
98
+
99
+ end
100
+
101
+ require 'circuit_breaker/circuit_handler'
102
+ require 'circuit_breaker/circuit_broken_exception'
103
+ require 'circuit_breaker/circuit_state'
@@ -0,0 +1,10 @@
1
+ class CircuitBreaker::CircuitBrokenException < StandardError
2
+
3
+ def initialize(msg, circuit_state)
4
+ @circuit_state = circuit_state
5
+ super(msg)
6
+ end
7
+
8
+ attr_reader :circuit_state
9
+
10
+ end
@@ -0,0 +1,135 @@
1
+ #
2
+ #
3
+ # CircuitHandler is stateless,
4
+ # so the circuit_state gets mixed in with the calling object.
5
+ #
6
+ #
7
+ class CircuitBreaker::CircuitHandler
8
+
9
+ #
10
+ # The number of failures needed to trip the breaker.
11
+ #
12
+ attr_accessor :failure_threshold
13
+
14
+ #
15
+ # The period of time in seconds before attempting to reset the breaker.
16
+ #
17
+ attr_accessor :failure_timeout
18
+
19
+ #
20
+ # Optional logger.
21
+ #
22
+ attr_accessor :logger
23
+
24
+ DEFAULT_FAILURE_THRESHOLD = 5
25
+ DEFAULT_FAILURE_TIMEOUT = 5
26
+
27
+ def initialize(logger = nil)
28
+ @logger = logger
29
+ @failure_threshold = DEFAULT_FAILURE_THRESHOLD
30
+ @failure_timeout = DEFAULT_FAILURE_TIMEOUT
31
+ end
32
+
33
+ #
34
+ # Returns a new CircuitState instance.
35
+ #
36
+ def new_circuit_state
37
+ ::CircuitBreaker::CircuitState.new
38
+ end
39
+
40
+ #
41
+ # Handles the method covered by the circuit breaker.
42
+ #
43
+ def handle(circuit_state, method, *args)
44
+ if is_tripped(circuit_state)
45
+ @logger.debug("handle: breaker is tripped, refusing to execute: #{circuit_state.inspect}") if @logger
46
+ on_circuit_open(circuit_state)
47
+ end
48
+
49
+ begin
50
+ out = method[*args]
51
+ on_success(circuit_state)
52
+ rescue Exception
53
+ on_failure(circuit_state)
54
+ raise
55
+ end
56
+ return out
57
+ end
58
+
59
+ #
60
+ # Returns true when the number of failures is sufficient to trip the breaker, false otherwise.
61
+ #
62
+ def is_failure_threshold_reached(circuit_state)
63
+ out = (circuit_state.failure_count > failure_threshold)
64
+ @logger.debug("is_failure_threshold_reached: #{circuit_state.failure_count} > #{failure_threshold} == #{out}") if @logger
65
+
66
+ return out
67
+ end
68
+
69
+ #
70
+ # Returns true if enough time has elapsed since the last failure time, false otherwise.
71
+ #
72
+ def is_timeout_exceeded(circuit_state)
73
+ now = Time.now
74
+
75
+ time_since = now - circuit_state.last_failure_time
76
+ @logger.debug("timeout_exceeded: time since last failure = #{time_since.inspect}") if @logger
77
+ return time_since >= failure_timeout
78
+ end
79
+
80
+ #
81
+ # Returns true if the circuit breaker is still open and the timeout has
82
+ # not been exceeded, false otherwise.
83
+ #
84
+ def is_tripped(circuit_state)
85
+
86
+ if circuit_state.open? && is_timeout_exceeded(circuit_state)
87
+ @logger.debug("is_tripped: attempting reset into half open state for #{circuit_state.inspect}") if @logger
88
+ circuit_state.attempt_reset
89
+ end
90
+
91
+ return circuit_state.open?
92
+ end
93
+
94
+ #
95
+ # Called when an individual success happens.
96
+ #
97
+ def on_success(circuit_state)
98
+ @logger.debug("on_success: #{circuit_state.inspect}") if @logger
99
+
100
+ if circuit_state.closed?
101
+ @logger.debug("on_success: reset_failure_count #{circuit_state.inspect}") if @logger
102
+ circuit_state.reset_failure_count
103
+ end
104
+
105
+ if circuit_state.half_open?
106
+ @logger.debug("on_success: reset circuit #{circuit_state.inspect}") if @logger
107
+ circuit_state.reset
108
+ end
109
+ end
110
+
111
+ #
112
+ # Called when an individual failure happens.
113
+ #
114
+ def on_failure(circuit_state)
115
+ @logger.debug("on_failure: circuit_state = #{circuit_state.inspect}") if @logger
116
+
117
+ circuit_state.increment_failure_count
118
+
119
+ if is_failure_threshold_reached(circuit_state) || circuit_state.half_open?
120
+ # Set us into a closed state.
121
+ @logger.debug("on_failure: tripping circuit breaker #{circuit_state.inspect}") if @logger
122
+ circuit_state.trip
123
+ end
124
+ end
125
+
126
+ #
127
+ # Called when a call is made and the circuit is open. Raises a CircuitBrokenException exception.
128
+ #
129
+ def on_circuit_open(circuit_state)
130
+ @logger.debug("on_circuit_open: raising for #{circuit_state.inspect}") if @logger
131
+
132
+ raise CircuitBreaker::CircuitBrokenException.new("Circuit broken, please wait for timeout", circuit_state)
133
+ end
134
+
135
+ end
@@ -0,0 +1,60 @@
1
+ require 'aasm'
2
+
3
+ #
4
+ # CircuitState is created individually for each object, and keeps
5
+ # track of how the object is doing and whether the object's circuit
6
+ # has tripped or not.
7
+ #
8
+ class CircuitBreaker::CircuitState
9
+
10
+ include AASM
11
+
12
+ aasm_state :half_open
13
+
14
+ aasm_state :open
15
+
16
+ aasm_state :closed, :enter => :reset_failure_count
17
+
18
+ aasm_initial_state :closed
19
+
20
+ #
21
+ # Trips the circuit breaker into the open state where it will immediately fail.
22
+ #
23
+ aasm_event :trip do
24
+ transitions :to => :open, :from => [:closed, :half_open]
25
+ end
26
+
27
+ #
28
+ # Transitions from an open state to a half_open state.
29
+ #
30
+ aasm_event :attempt_reset do
31
+ transitions :to => :half_open, :from => [:open]
32
+ end
33
+
34
+ #
35
+ # Close the circuit from an open or half open state.
36
+ #
37
+ aasm_event :reset do
38
+ transitions :to => :closed, :from => [:open, :half_open]
39
+ end
40
+
41
+ def initialize()
42
+ @failure_count = 0
43
+ @last_failure_time = nil
44
+ end
45
+
46
+ attr_accessor :last_failure_time
47
+
48
+ attr_accessor :failure_count
49
+
50
+ def increment_failure_count
51
+ @failure_count = @failure_count + 1
52
+ @last_failure_time = Time.now
53
+ end
54
+
55
+ def reset_failure_count
56
+ @failure_count = 0
57
+ end
58
+
59
+ end
60
+
@@ -0,0 +1,150 @@
1
+ require File.dirname(__FILE__) + '/../unit_spec_helper'
2
+
3
+ describe CircuitBreaker do
4
+
5
+ class TestClass
6
+
7
+ include CircuitBreaker
8
+
9
+ def initialize()
10
+ @failure = false
11
+ end
12
+
13
+ def fail!
14
+ @failure = true
15
+ end
16
+
17
+ def succeed!
18
+ @failure = false
19
+ end
20
+
21
+ def call_external_method()
22
+ if @failure == true
23
+ raise "FAIL"
24
+ end
25
+
26
+ "hello world!"
27
+ end
28
+
29
+ # Register this method with the circuit breaker...
30
+ #
31
+ circuit_method :call_external_method
32
+
33
+ #
34
+ # Define what needs to be set for configuration...
35
+ #
36
+ circuit_handler do |handler|
37
+ handler.logger = Logger.new(STDOUT)
38
+ handler.failure_threshold = 5
39
+ handler.failure_timeout = 5
40
+ end
41
+
42
+ end
43
+
44
+ before(:each) do
45
+ @test_object = TestClass.new()
46
+ end
47
+
48
+ describe "when closed" do
49
+
50
+ it "should execute without failing" do
51
+ @test_object.call_external_method().should == 'hello world!'
52
+ @test_object.circuit_state.closed? == true
53
+ @test_object.circuit_state.failure_count == 0
54
+ end
55
+
56
+ it 'should increment the failure count when a failure occurs' do
57
+ @test_object.fail!
58
+
59
+ lambda { @test_object.call_external_method() }.should raise_error
60
+ @test_object.circuit_state.closed? == true
61
+ @test_object.circuit_state.failure_count == 1
62
+ end
63
+
64
+ it 'should trip the circuit when too many failures occur' do
65
+ @test_object.fail!
66
+
67
+ lambda { @test_object.call_external_method() }.should raise_error
68
+ lambda { @test_object.call_external_method() }.should raise_error
69
+ lambda { @test_object.call_external_method() }.should raise_error
70
+ lambda { @test_object.call_external_method() }.should raise_error
71
+ lambda { @test_object.call_external_method() }.should raise_error
72
+ lambda { @test_object.call_external_method() }.should raise_error
73
+
74
+ @test_object.circuit_state.open?.should == true
75
+ @test_object.circuit_state.failure_count.should == 6
76
+ end
77
+
78
+ it 'should reset the failure count if closed after a successful call.' do
79
+ @test_object.fail!
80
+
81
+ lambda { @test_object.call_external_method() }.should raise_error("FAIL")
82
+
83
+ @test_object.succeed!
84
+ @test_object.call_external_method()
85
+
86
+ @test_object.circuit_state.failure_count.should == 0
87
+ @test_object.circuit_state.closed?.should == true
88
+
89
+ end
90
+
91
+ end
92
+
93
+ describe "when open" do
94
+
95
+ it 'should fail immediately if the circuit is open' do
96
+ now = Time.now
97
+ @test_object.circuit_state.trip!
98
+ @test_object.circuit_state.last_failure_time = now
99
+ @test_object.circuit_state.failure_count = 5
100
+
101
+ # Should return CircuitBrokenException explicitly
102
+ lambda { @test_object.call_external_method() }.should raise_error(::CircuitBreaker::CircuitBrokenException)
103
+
104
+ # Failure count should not be open
105
+ @test_object.circuit_state.failure_count.should == 5
106
+ @test_object.circuit_state.open?.should == true
107
+ end
108
+
109
+ end
110
+
111
+ describe "when half open" do
112
+
113
+ it 'should reset the circuit when enough time has passed' do
114
+ now = Time.now
115
+
116
+ @test_object.circuit_state.trip
117
+ @test_object.circuit_state.attempt_reset
118
+ @test_object.circuit_state.last_failure_time = now - 5
119
+ @test_object.circuit_state.failure_count = 5
120
+
121
+ @test_object.call_external_method()
122
+
123
+ # After a successful call, the failure count is reset.
124
+ @test_object.circuit_state.failure_count.should == 0
125
+ @test_object.circuit_state.closed?.should == true
126
+ end
127
+
128
+ it 'should trip the circuit immediately if in a half open state' do
129
+ now = Time.now
130
+
131
+ @test_object.circuit_state.trip
132
+
133
+ # Set to half open...
134
+ @test_object.circuit_state.attempt_reset
135
+ @test_object.circuit_state.last_failure_time = now - 5
136
+ @test_object.circuit_state.failure_count = 5
137
+
138
+ # Have an unsuccessful call...
139
+ @test_object.fail!
140
+ lambda { @test_object.call_external_method() }.should raise_error("FAIL")
141
+
142
+ @test_object.circuit_state.failure_count.should == 6
143
+
144
+ # The circuit should immediately pop open again.
145
+ @test_object.circuit_state.open?.should == true
146
+ end
147
+
148
+ end
149
+
150
+ end
@@ -0,0 +1,5 @@
1
+ $:.unshift File.join(File.dirname(__FILE__),'..','lib')
2
+
3
+ require 'spec'
4
+ require 'circuit_breaker'
5
+
metadata ADDED
@@ -0,0 +1,98 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: wsargent-circuit_breaker
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Will Sargent
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-07-13 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: rubyist-aasm
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: rspec
27
+ type: :development
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 1.2.7
34
+ version:
35
+ - !ruby/object:Gem::Dependency
36
+ name: hoe
37
+ type: :development
38
+ version_requirement:
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: 2.3.1
44
+ version:
45
+ description: "CircuitBreaker is a relatively simple Ruby mixin that will wrap a call to a given service in a circuit breaker pattern. The circuit starts off \"closed\" meaning that all calls will go through. However, consecutive failures are recorded and after a threshold is reached, the circuit will \"trip\", setting the circuit into an \"open\" state. In an \"open\" state, every call to the service will fail by raising CircuitBrokenException. The circuit will remain in an \"open\" state until the failure timeout has elapsed. After the failure_timeout has elapsed, the circuit will go into a \"half open\" state and the call will go through. A failure will immediately pop the circuit open again, and a success will close the circuit and reset the failure count. require 'circuit_breaker' class TestService include CircuitBreaker def call_remote_service() ... circuit_method :call_remote_service # Optional circuit_handler do |handler| handler.logger = Logger.new(STDOUT) handler.failure_threshold = 5 handler.failure_timeout = 5 end # Optional circuit_handler_class MyCustomCircuitHandler end"
46
+ email:
47
+ - will.sargent@gmail.com
48
+ executables: []
49
+
50
+ extensions: []
51
+
52
+ extra_rdoc_files:
53
+ - History.txt
54
+ - Manifest.txt
55
+ - README.txt
56
+ files:
57
+ - History.txt
58
+ - Manifest.txt
59
+ - README.txt
60
+ - Rakefile
61
+ - circuit_breaker.gemspec
62
+ - lib/circuit_breaker.rb
63
+ - lib/circuit_breaker/circuit_state.rb
64
+ - lib/circuit_breaker/circuit_handler.rb
65
+ - lib/circuit_breaker/circuit_broken_exception.rb
66
+ - spec/unit_spec_helper.rb
67
+ - spec/unit/circuit_breaker_spec.rb
68
+ has_rdoc: false
69
+ homepage: http://github.com/wsargent/circuit_breaker
70
+ post_install_message:
71
+ rdoc_options:
72
+ - --main
73
+ - README.txt
74
+ - --charset=UTF-8
75
+ require_paths:
76
+ - lib
77
+ required_ruby_version: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: "0"
82
+ version:
83
+ required_rubygems_version: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - ">="
86
+ - !ruby/object:Gem::Version
87
+ version: "0"
88
+ version:
89
+ requirements: []
90
+
91
+ rubyforge_project: will_sargent
92
+ rubygems_version: 1.2.0
93
+ signing_key:
94
+ specification_version: 3
95
+ summary: CircuitBreaker is a relatively simple Ruby mixin that will wrap a call to a given service in a circuit breaker pattern
96
+ test_files:
97
+ - spec/unit/circuit_breaker_spec.rb
98
+ - spec/unit_spec_helper.rb