circuit_b 1.0 → 1.1

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.
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