circuit_b 1.0 → 1.1

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.md ADDED
@@ -0,0 +1,6 @@
1
+ Version 1.1
2
+ ===========
3
+
4
+ * Added on-break handling features
5
+ * Fixed the default fuse configuration handling
6
+ * Added internal code execution timeouts
data/README.md CHANGED
@@ -38,9 +38,11 @@ Configuration
38
38
  # Configure the default fuse configuration that will be
39
39
  # used as the basis when you add your custom fuses. You
40
40
  # can specify only the parameters you want to override then.
41
- config.default_fuse_config = {
41
+ c.default_fuse_config = {
42
+ :on_break => [ :rails_log, lambda { do_something } ],
42
43
  :allowed_failures => 2,
43
44
  :cool_off_period => 3 # seconds
45
+ :timeout => 3 # seconds, defaults to 5
44
46
  }
45
47
 
46
48
  # Adds a fuse named "mail" that is configured to tolerate
@@ -48,7 +50,7 @@ Configuration
48
50
  # of 60 seconds it will close again. During the cool-off
49
51
  # time it will be raising FastFailure's without even
50
52
  # executing the code to protect the system from overload.
51
- c.add_fuse "mail", :allowed_failures => 5, :cool_off_period => 60
53
+ c.fuse "mail", :allowed_failures => 5, :cool_off_period => 60
52
54
 
53
55
  end
54
56
 
@@ -68,6 +70,48 @@ the fuse state that you can use:
68
70
  and acts like a simple IPC.
69
71
 
70
72
 
73
+ Acting on Fuse Breaks
74
+ =====================
75
+
76
+ When the ciruit is broken, meaning that your wrapped code has produced
77
+ so many errors that we had to isolate it, the fuse opens and starts to
78
+ fail fast. You may want to act in one way or another when it happens.
79
+ There's a fuse configuration option `on_break` that accepts one or more
80
+ elements describing what you want to do.
81
+
82
+ *Logging.* One of the common steps is to log the event. There's a
83
+ standard logging feature (`:rails_log`) that writes a message to the
84
+ default Rails log. If you don't use Rails, you can use the next feature
85
+ to take care of your logging.
86
+
87
+ config.fuse "test", :on_break => :rails_log
88
+
89
+ *Handling.* If you want to handle the event in some custom way, you
90
+ can provide a `Proc` that will be executed upon event. One common case
91
+ is to write to the log.
92
+
93
+ config.fuse "test", :on_break => lambda { |fuse| puts "Fuse #{fuse.name} has just broke the circuit" }
94
+
95
+ To specify more than one handler, you can use an array:
96
+
97
+ config.fuse "test", :on_break => [ :rails_log, lambda { ... }, lambda { ... } ]
98
+
99
+
100
+ Code execution timeouts
101
+ =======================
102
+
103
+ To protect your code from executing for too long, fuses in CircuitB can
104
+ execute it wrapped into the timeout statements. All you have to do is
105
+ to configure a fuse to use timeouts logic, like this (to allow 5 second for
106
+ wrapped code execution):
107
+
108
+ config.fuse "test", :timeout => 2
109
+
110
+ To disable timeouts (which isn't a great idea), use `:timeout => false`.
111
+
112
+ By default, all fuses use 5 second timeouts.
113
+
114
+
71
115
  Usage
72
116
  =====
73
117
 
@@ -89,9 +133,7 @@ it all in one place.
89
133
  To Do
90
134
  =====
91
135
 
92
- * notifications and logging
93
136
  * half-open state to open back faster if the problem still exists
94
- * internal code block execution timeout support
95
137
  * incrementing cool-off period on recurring errors (in half-open state)
96
138
  * CouchDB storage
97
139
  * Memcached storage
data/Rakefile ADDED
@@ -0,0 +1,31 @@
1
+ # encoding: utf-8
2
+ require 'rubygems'
3
+ require 'rake'
4
+ $:.unshift(File.dirname(__FILE__) + '/lib')
5
+
6
+ begin
7
+ require 'jeweler'
8
+ Jeweler::Tasks.new do |gem|
9
+ gem.name = "circuit_b"
10
+ gem.version = "1.1"
11
+ gem.summary = %Q{Distributed circuit breaker}
12
+ gem.description = %Q{Classic circuit breaker to protect resources from being accessed over and over while in pain.}
13
+ gem.email = "spyromus@noizeramp.com"
14
+ gem.homepage = "http://github.com/alg/circuit_b"
15
+ gem.authors = ["Aleksey Gureiev"]
16
+
17
+ gem.add_development_dependency 'shoulda', '>= 2.10.3'
18
+ gem.add_development_dependency 'timecop', '>= 0.3.4'
19
+ end
20
+
21
+ Jeweler::GemcutterTasks.new
22
+ rescue LoadError
23
+ puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
24
+ end
25
+
26
+ Dir['gem_tasks/**/*.rake'].each { |rake| load rake }
27
+
28
+ task :default => [:check_dependencies, :test]
29
+
30
+ require 'rake/clean'
31
+ CLEAN.include %w(**/*.{log,pyc})
data/circuit_b.gemspec ADDED
@@ -0,0 +1,64 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{circuit_b}
8
+ s.version = "1.1"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Aleksey Gureiev"]
12
+ s.date = %q{2010-02-04}
13
+ s.description = %q{Classic circuit breaker to protect resources from being accessed over and over while in pain.}
14
+ s.email = %q{spyromus@noizeramp.com}
15
+ s.extra_rdoc_files = [
16
+ "README.md"
17
+ ]
18
+ s.files = [
19
+ "CHANGELOG.md",
20
+ "MIT-LICENSE",
21
+ "README.md",
22
+ "Rakefile",
23
+ "circuit_b.gemspec",
24
+ "lib/circuit_b.rb",
25
+ "lib/circuit_b/configuration.rb",
26
+ "lib/circuit_b/fuse.rb",
27
+ "lib/circuit_b/storage.rb",
28
+ "lib/circuit_b/storage/base.rb",
29
+ "lib/circuit_b/storage/memory.rb",
30
+ "lib/circuit_b/storage/redis.rb",
31
+ "test/test_helper.rb",
32
+ "test/unit/circuit_b/test_configuration.rb",
33
+ "test/unit/circuit_b/test_fuse.rb",
34
+ "test/unit/test_circuit_b.rb"
35
+ ]
36
+ s.homepage = %q{http://github.com/alg/circuit_b}
37
+ s.rdoc_options = ["--charset=UTF-8"]
38
+ s.require_paths = ["lib"]
39
+ s.rubygems_version = %q{1.3.5}
40
+ s.summary = %q{Distributed circuit breaker}
41
+ s.test_files = [
42
+ "test/test_helper.rb",
43
+ "test/unit/circuit_b/test_configuration.rb",
44
+ "test/unit/circuit_b/test_fuse.rb",
45
+ "test/unit/test_circuit_b.rb"
46
+ ]
47
+
48
+ if s.respond_to? :specification_version then
49
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
50
+ s.specification_version = 3
51
+
52
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
53
+ s.add_development_dependency(%q<shoulda>, [">= 2.10.3"])
54
+ s.add_development_dependency(%q<timecop>, [">= 0.3.4"])
55
+ else
56
+ s.add_dependency(%q<shoulda>, [">= 2.10.3"])
57
+ s.add_dependency(%q<timecop>, [">= 0.3.4"])
58
+ end
59
+ else
60
+ s.add_dependency(%q<shoulda>, [">= 2.10.3"])
61
+ s.add_dependency(%q<timecop>, [">= 0.3.4"])
62
+ end
63
+ end
64
+
@@ -5,6 +5,8 @@ module CircuitB
5
5
  class Configuration
6
6
 
7
7
  DEFAULT_CONFIG = {
8
+ :log => true,
9
+ :timeout => 5, # seconds
8
10
  :allowed_failures => 5,
9
11
  :cool_off_period => 10 # seconds
10
12
  }
@@ -19,14 +21,34 @@ module CircuitB
19
21
  @fuses = {}
20
22
  end
21
23
 
24
+ # Sets the default fuse configuration. This configuration will be used
25
+ # as the basis for all fuses. You can override the values by providing
26
+ # your own when calling #fuse.
27
+ #
28
+ # CircuitB.configure do |c|
29
+ # c.default_fuse_config = {
30
+ # :on_break => [ :rails_log, lambda { do_something } ],
31
+ # :allowed_failures => 2,
32
+ # :cool_off_period => 3 # seconds
33
+ # }
34
+ # end
22
35
  def default_fuse_config=(config)
23
36
  @default_fuse_config = DEFAULT_CONFIG.merge(config)
24
37
  end
25
38
 
26
- def add_fuse(name, config = {})
39
+ # Adds a fuse with a given name and custom config.
40
+ # If the fuse with the same name is already there, the RuntimeError is raised.
41
+ # The values of the provided configuration are used to override
42
+ # the default configuration that can be set with #default_fuse_config.
43
+ #
44
+ # CircuitB.configure do |c|
45
+ # c.fuse "directory-auth", :on_break => lambda { notify_hoptoad(...) }, :allowed_failures => 5
46
+ # c.fuse "image-resizing", :allowed_failures => 2, :cool_off_period => 30
47
+ # end
48
+ def fuse(name, config = {})
27
49
  raise "Fuse with this name is already registered" if @fuses.include?(name)
28
50
 
29
- config = DEFAULT_CONFIG.merge(config || {})
51
+ config = @default_fuse_config.merge(config || {})
30
52
  @fuses[name] = CircuitB::Fuse.new(name, state_storage, config)
31
53
  end
32
54
 
@@ -3,7 +3,16 @@ require 'circuit_b/storage/base'
3
3
  module CircuitB
4
4
  class Fuse
5
5
 
6
- attr_reader :config
6
+ # Maximum time the handler is allowed to execute
7
+ DEFAULT_BREAK_HANDLER_TIMEOUT = 5
8
+
9
+ # Standard handlers that can be refered by their names
10
+ STANDARD_HANDLERS = {
11
+ :rails_log => lambda { |fuse| RAILS_DEFAULT_LOGGER.error("CircuitB: Fuse '#{fuse.name}' has broken") }
12
+ }
13
+
14
+ attr_reader :name, :config
15
+ attr_accessor :break_handler_timeout
7
16
 
8
17
  def initialize(name, state_storage, config)
9
18
  raise "Name must be specified" if name.nil?
@@ -14,6 +23,8 @@ module CircuitB
14
23
  @name = name
15
24
  @state_storage = state_storage
16
25
  @config = config
26
+
27
+ @break_handler_timeout = DEFAULT_BREAK_HANDLER_TIMEOUT
17
28
  end
18
29
 
19
30
  def wrap(&block)
@@ -21,10 +32,14 @@ module CircuitB
21
32
  raise CircuitB::FastFailure if open?
22
33
 
23
34
  begin
24
- block.call
35
+ if @config[:timeout] && @config[:timeout].to_f > 0
36
+ Timeout::timeout(@config[:timeout].to_f) { block.call }
37
+ else
38
+ block.call
39
+ end
25
40
 
26
41
  put(:failures, 0)
27
- rescue => e
42
+ rescue Exception => e
28
43
  # Save the time of the last failure
29
44
  put(:last_failure_at, Time.now.to_i)
30
45
 
@@ -57,6 +72,24 @@ module CircuitB
57
72
  # Open the fuse
58
73
  def open
59
74
  put(:state, :open)
75
+
76
+ if config[:on_break]
77
+ require 'timeout'
78
+
79
+ handlers = [ config[:on_break] ].flatten.map { |handler| (handler.is_a?(Symbol) ? STANDARD_HANDLERS[handler] : handler) }.compact
80
+
81
+ handlers.each do |handler|
82
+ begin
83
+ Timeout::timeout(@break_handler_timeout) {
84
+ handler.call(self)
85
+ }
86
+ rescue Timeout::Error
87
+ # We ignore handler timeouts
88
+ rescue
89
+ # We ignore handler errors
90
+ end
91
+ end
92
+ end
60
93
  end
61
94
 
62
95
  def get(field)
@@ -19,18 +19,18 @@ class CircuitB::TestConfiguration < Test::Unit::TestCase
19
19
  end
20
20
 
21
21
  should "add a named fuse with default configuration" do
22
- @config.add_fuse "fuse_name"
22
+ @config.fuse "fuse_name"
23
23
  assert_equal 1, @config.fuses.size
24
24
  end
25
25
 
26
26
  should "add a named fuse with custom configuration" do
27
- @config.add_fuse "fuse_name", :allowed_failures => 5
27
+ @config.fuse "fuse_name", :allowed_failures => 5
28
28
  end
29
29
 
30
30
  should "disallow adding fuses with the same name" do
31
- @config.add_fuse "fuse_name"
31
+ @config.fuse "fuse_name"
32
32
  begin
33
- @config.add_fuse "fuse_name"
33
+ @config.fuse "fuse_name"
34
34
  fail "should raise an exception"
35
35
  rescue
36
36
  # Exception is expected
@@ -43,7 +43,7 @@ class CircuitB::TestFuse < Test::Unit::TestCase
43
43
 
44
44
  context "operation" do
45
45
  setup do
46
- @fuse = CircuitB::Fuse.new("name", CircuitB::Storage::Memory.new, :allowed_failures => 1, :cool_off_period => 60)
46
+ @fuse = memory_fuse
47
47
  end
48
48
 
49
49
  should "open when the allowed failures reached" do
@@ -53,7 +53,7 @@ class CircuitB::TestFuse < Test::Unit::TestCase
53
53
  end
54
54
 
55
55
  should "reset the failures counter when the attempt succeeds" do
56
- @fuse = CircuitB::Fuse.new("name", CircuitB::Storage::Memory.new, :allowed_failures => 2)
56
+ @fuse = memory_fuse(:allowed_failures => 2)
57
57
 
58
58
  do_failure(@fuse)
59
59
  assert_equal 1, @fuse.failures
@@ -109,6 +109,65 @@ class CircuitB::TestFuse < Test::Unit::TestCase
109
109
  assert !@fuse.open?
110
110
  end
111
111
  end
112
+
113
+ context "on-break handlers" do
114
+ should "call single handler" do
115
+ handler_fuse = nil
116
+ handler = lambda { |fuse| handler_fuse = fuse }
117
+ @fuse = memory_fuse(:on_break => handler)
118
+
119
+ do_failure(@fuse)
120
+
121
+ assert_equal @fuse, handler_fuse
122
+ end
123
+
124
+ should "call all of handlers" do
125
+ handler_calls = 0
126
+ handler = lambda { |fuse| handler_calls += 1 }
127
+ @fuse = memory_fuse(:on_break => [ handler, handler ])
128
+
129
+ do_failure(@fuse)
130
+
131
+ assert_equal 2, handler_calls
132
+ end
133
+
134
+ should "ignore failures of handlers" do
135
+ handler_calls = 0
136
+ handler = lambda { |fuse| handler_calls += 1 }
137
+ failing_handler = lambda { |fuse| raise "Handling error" }
138
+ @fuse = memory_fuse(:on_break => [ failing_handler, handler ])
139
+
140
+ do_failure(@fuse)
141
+
142
+ assert_equal 1, handler_calls
143
+ end
144
+
145
+ should "interrupt long handlers (no more than 5 seconds)" do
146
+ handler_calls = 0
147
+ long_handler = lambda { |fuse| sleep 10; handler_calls += 1 }
148
+ short_handler = lambda { |fuse| handler_calls += 1 }
149
+ @fuse = memory_fuse(:on_break => [ long_handler, short_handler ])
150
+ @fuse.break_handler_timeout = 0.1
151
+
152
+ do_failure(@fuse)
153
+
154
+ assert_equal 1, handler_calls
155
+ end
156
+ end
157
+
158
+ context "execution timeouts" do
159
+ should "fail long tasks" do
160
+ @fuse = memory_fuse(:timeout => 0.1)
161
+ begin
162
+ @fuse.wrap do
163
+ sleep 0.2
164
+ end
165
+ fail "Timeout::Error should be thrown"
166
+ rescue Timeout::Error
167
+ assert @fuse.open?
168
+ end
169
+ end
170
+ end
112
171
  end
113
172
 
114
173
  def do_failure(fuse, rethrow = false)
@@ -120,4 +179,9 @@ class CircuitB::TestFuse < Test::Unit::TestCase
120
179
  raise e if rethrow
121
180
  end
122
181
  end
182
+
183
+ def memory_fuse(options = {})
184
+ options = { :allowed_failures => 1, :cool_off_period => 60 }.merge(options)
185
+ CircuitB::Fuse.new("name", CircuitB::Storage::Memory.new, options)
186
+ end
123
187
  end
@@ -15,7 +15,7 @@ class TestCircuitB < Test::Unit::TestCase
15
15
  :cool_off_period => 3 # seconds
16
16
  }
17
17
 
18
- config.add_fuse("mail", {
18
+ config.fuse("mail", {
19
19
  :allowed_failures => 5,
20
20
  :cool_off_period => 10 # seconds
21
21
  })
@@ -30,10 +30,16 @@ class TestCircuitB < Test::Unit::TestCase
30
30
 
31
31
  context "using fuses to protect code" do
32
32
  setup do
33
+ begin
34
+ CircuitB::Storage::Redis.new.get('dummy', 'field')
35
+ rescue Errno::ECONNREFUSED => e
36
+ fail "Please start Redis on default port"
37
+ end
38
+
33
39
  CircuitB.reset_configuration
34
40
  CircuitB.configure do |c|
35
41
  c.state_storage = CircuitB::Storage::Redis.new
36
- c.add_fuse "fuse_name", :allowed_failures => 1, :cool_off_period => 10
42
+ c.fuse "fuse_name", :allowed_failures => 1, :cool_off_period => 10
37
43
  end
38
44
  end
39
45
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: circuit_b
3
3
  version: !ruby/object:Gem::Version
4
- version: "1.0"
4
+ version: "1.1"
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aleksey Gureiev
@@ -9,10 +9,29 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2010-01-30 00:00:00 +11:00
12
+ date: 2010-02-04 00:00:00 +11:00
13
13
  default_executable:
14
- dependencies: []
15
-
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: shoulda
17
+ type: :development
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 2.10.3
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: timecop
27
+ type: :development
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 0.3.4
34
+ version:
16
35
  description: Classic circuit breaker to protect resources from being accessed over and over while in pain.
17
36
  email: spyromus@noizeramp.com
18
37
  executables: []
@@ -20,30 +39,31 @@ executables: []
20
39
  extensions: []
21
40
 
22
41
  extra_rdoc_files:
23
- - MIT-LICENSE
24
42
  - README.md
25
43
  files:
44
+ - CHANGELOG.md
45
+ - MIT-LICENSE
46
+ - README.md
47
+ - Rakefile
48
+ - circuit_b.gemspec
49
+ - lib/circuit_b.rb
26
50
  - lib/circuit_b/configuration.rb
27
51
  - lib/circuit_b/fuse.rb
52
+ - lib/circuit_b/storage.rb
28
53
  - lib/circuit_b/storage/base.rb
29
54
  - lib/circuit_b/storage/memory.rb
30
55
  - lib/circuit_b/storage/redis.rb
31
- - lib/circuit_b/storage.rb
32
- - lib/circuit_b.rb
33
56
  - test/test_helper.rb
34
57
  - test/unit/circuit_b/test_configuration.rb
35
58
  - test/unit/circuit_b/test_fuse.rb
36
59
  - test/unit/test_circuit_b.rb
37
- - MIT-LICENSE
38
- - README.md
39
60
  has_rdoc: true
40
61
  homepage: http://github.com/alg/circuit_b
41
62
  licenses: []
42
63
 
43
64
  post_install_message:
44
65
  rdoc_options:
45
- - --title
46
- - CircuitB - Distributed circuit breaker
66
+ - --charset=UTF-8
47
67
  require_paths:
48
68
  - lib
49
69
  required_ruby_version: !ruby/object:Gem::Requirement
@@ -65,5 +85,8 @@ rubygems_version: 1.3.5
65
85
  signing_key:
66
86
  specification_version: 3
67
87
  summary: Distributed circuit breaker
68
- test_files: []
69
-
88
+ test_files:
89
+ - test/test_helper.rb
90
+ - test/unit/circuit_b/test_configuration.rb
91
+ - test/unit/circuit_b/test_fuse.rb
92
+ - test/unit/test_circuit_b.rb