circuit_b 1.0

Sign up to get free protection for your applications and to get access to all the features.
data/MIT-LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright (c) 2010 Aleksey Gureiev
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,104 @@
1
+ Theory
2
+ ======
3
+
4
+ When you are accessing some resource that is known to be unreliable,
5
+ it's better to wrap your requests with a circuit breaking logic.
6
+ The breaker acts as a fuse. When it senses that all your requests
7
+ end up with errors, it breaks the circuit and starts throwing fail-fast
8
+ errors instead without even trying to execute the code block in question.
9
+ Often it gives enough time for the resource (mail server, directory service,
10
+ router etc) to recover and resume normal operation.
11
+
12
+ After a certain period of time, circuit breaker attempts to restore
13
+ the link and, if it sees that the problem is still there, it breaks it
14
+ again.
15
+
16
+
17
+ Installation
18
+ ============
19
+
20
+ gem install circuit_b
21
+
22
+
23
+ Configuration
24
+ =============
25
+
26
+ CircuitB.configure do |c|
27
+
28
+ # Configure the storage that will be used to store the
29
+ # state of the fuses across multiple invocations.
30
+ # There are Memory- and Redis-based stores:
31
+ # - Memore store is good when you don't have
32
+ # several threads working with the same fuse,
33
+ # like in Rails or other multi-threaded environments.
34
+ # - Redis store is good for shared multi-threaded
35
+ # environments.
36
+ c.state_storage = CircuitB::Storage::Redis.new
37
+
38
+ # Configure the default fuse configuration that will be
39
+ # used as the basis when you add your custom fuses. You
40
+ # can specify only the parameters you want to override then.
41
+ config.default_fuse_config = {
42
+ :allowed_failures => 2,
43
+ :cool_off_period => 3 # seconds
44
+ }
45
+
46
+ # Adds a fuse named "mail" that is configured to tolerate
47
+ # 5 failures before opening. After the cool off period
48
+ # of 60 seconds it will close again. During the cool-off
49
+ # time it will be raising FastFailure's without even
50
+ # executing the code to protect the system from overload.
51
+ c.add_fuse "mail", :allowed_failures => 5, :cool_off_period => 60
52
+
53
+ end
54
+
55
+
56
+ Available storages
57
+ ==================
58
+
59
+ In order to share the state between co-named fuses, one needs to use
60
+ the storage of the correct type. There are currently two storages for
61
+ the fuse state that you can use:
62
+
63
+ * _CircuitB::Storage::Memory_ -- the simplest memory-based storage.
64
+ Ideal for the single-threaded situations.
65
+
66
+ * _CircuitB::Storage::Redis_ -- Redis-based storage. Well-suited
67
+ for distributed setups (like multiple workers in Rails and alike)
68
+ and acts like a simple IPC.
69
+
70
+
71
+ Usage
72
+ =====
73
+
74
+ Every time you want to protect a piece of code, you do this:
75
+
76
+ CircuitB("mail") do
77
+ # Attempting to send mail
78
+ end
79
+
80
+ Note, that in order to use "mail" fuse you need to add it to your
81
+ configuration first (see above).
82
+
83
+ You can use fuses in any number of places, but since the state is
84
+ shared across all fuses with the same name, make sure you use them
85
+ for the same purpose, or better yet, refactor your code to have
86
+ it all in one place.
87
+
88
+
89
+ To Do
90
+ =====
91
+
92
+ * notifications and logging
93
+ * half-open state to open back faster if the problem still exists
94
+ * internal code block execution timeout support
95
+ * incrementing cool-off period on recurring errors (in half-open state)
96
+ * CouchDB storage
97
+ * Memcached storage
98
+ * passing storage configuration through the initializer
99
+
100
+ License
101
+ =======
102
+
103
+ Circuit Breaker is Copyright © 2010 [Aleksey Gureiev](mailto:spyromus@noizeramp.com).
104
+ It is free software, and may be redistributed under the terms specified in the MIT-LICENSE file.
@@ -0,0 +1,34 @@
1
+ require "circuit_b/storage"
2
+ require "circuit_b/fuse"
3
+
4
+ module CircuitB
5
+ class Configuration
6
+
7
+ DEFAULT_CONFIG = {
8
+ :allowed_failures => 5,
9
+ :cool_off_period => 10 # seconds
10
+ }
11
+
12
+ attr_accessor :state_storage
13
+ attr_reader :default_fuse_config
14
+ attr_reader :fuses
15
+
16
+ def initialize
17
+ @state_storage = CircuitB::Storage::Memory.new
18
+ @default_fuse_config = DEFAULT_CONFIG.clone
19
+ @fuses = {}
20
+ end
21
+
22
+ def default_fuse_config=(config)
23
+ @default_fuse_config = DEFAULT_CONFIG.merge(config)
24
+ end
25
+
26
+ def add_fuse(name, config = {})
27
+ raise "Fuse with this name is already registered" if @fuses.include?(name)
28
+
29
+ config = DEFAULT_CONFIG.merge(config || {})
30
+ @fuses[name] = CircuitB::Fuse.new(name, state_storage, config)
31
+ end
32
+
33
+ end
34
+ end
@@ -0,0 +1,74 @@
1
+ require 'circuit_b/storage/base'
2
+
3
+ module CircuitB
4
+ class Fuse
5
+
6
+ attr_reader :config
7
+
8
+ def initialize(name, state_storage, config)
9
+ raise "Name must be specified" if name.nil?
10
+ raise "Storage must be specified" if state_storage.nil?
11
+ raise "Storage must be of CircuitB::Storage::Base kind" unless state_storage.kind_of?(CircuitB::Storage::Base)
12
+ raise "Config must be specified" if config.nil?
13
+
14
+ @name = name
15
+ @state_storage = state_storage
16
+ @config = config
17
+ end
18
+
19
+ def wrap(&block)
20
+ close_if_cooled_off if open?
21
+ raise CircuitB::FastFailure if open?
22
+
23
+ begin
24
+ block.call
25
+
26
+ put(:failures, 0)
27
+ rescue => e
28
+ # Save the time of the last failure
29
+ put(:last_failure_at, Time.now.to_i)
30
+
31
+ # Increment the number of failures and open if the limit has been reached
32
+ failures = inc(:failures)
33
+ open if failures >= @config[:allowed_failures]
34
+
35
+ # Re-raise the original exception
36
+ raise e
37
+ end
38
+ end
39
+
40
+ def open?
41
+ get(:state) == :open
42
+ end
43
+
44
+ def failures
45
+ get(:failures)
46
+ end
47
+
48
+ private
49
+
50
+ def close_if_cooled_off
51
+ if Time.now.to_i - get(:last_failure_at).to_i > config[:cool_off_period]
52
+ put(:state, :closed)
53
+ put(:failures, 0)
54
+ end
55
+ end
56
+
57
+ # Open the fuse
58
+ def open
59
+ put(:state, :open)
60
+ end
61
+
62
+ def get(field)
63
+ @state_storage.get(@name, field)
64
+ end
65
+
66
+ def put(field, value)
67
+ @state_storage.put(@name, field, value)
68
+ end
69
+
70
+ def inc(field)
71
+ @state_storage.inc(@name, field)
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,18 @@
1
+ module CircuitB
2
+ module Storage
3
+ class Base
4
+
5
+ def put(fuse_name, field, value)
6
+ raise "Unimplemented"
7
+ end
8
+
9
+ def get(fuse_name, field)
10
+ raise "Unimplemented"
11
+ end
12
+
13
+ def inc(fuse_name, field)
14
+ raise "Unimplemented"
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,27 @@
1
+ require 'circuit_b/storage/base'
2
+
3
+ module CircuitB
4
+ module Storage
5
+ class Memory < Base
6
+
7
+ def initialize
8
+ @fuse_states = {}
9
+ end
10
+
11
+ def put(fuse_name, field, value)
12
+ @fuse_states[fuse_name] ||= {}
13
+ @fuse_states[fuse_name][field.to_sym] = value
14
+ end
15
+
16
+ def get(fuse_name, field)
17
+ (@fuse_states[fuse_name] || {})[field.to_sym]
18
+ end
19
+
20
+ def inc(fuse_name, field)
21
+ new_val = get(fuse_name, field).to_i + 1
22
+ put(fuse_name, field, new_val)
23
+ return new_val
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,31 @@
1
+ require 'circuit_b/storage/base'
2
+ require 'redis'
3
+
4
+ module CircuitB
5
+ module Storage
6
+ class Redis < Base
7
+
8
+ def initialize
9
+ @redis = ::Redis.new
10
+ end
11
+
12
+ def put(fuse_name, field, value)
13
+ @redis[key(fuse_name, field)] = value
14
+ end
15
+
16
+ def get(fuse_name, field)
17
+ @redis[key(fuse_name, field)]
18
+ end
19
+
20
+ def inc(fuse_name, field)
21
+ return @redis.incr(key(fuse_name, field))
22
+ end
23
+
24
+ private
25
+
26
+ def key(fuse_name, field)
27
+ "circuit_b:#{fuse_name}:#{field}"
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,2 @@
1
+ require "circuit_b/storage/memory"
2
+ require "circuit_b/storage/redis"
data/lib/circuit_b.rb ADDED
@@ -0,0 +1,34 @@
1
+ require "circuit_b/fuse"
2
+ require "circuit_b/configuration"
3
+ require "circuit_b/storage"
4
+
5
+ module CircuitB
6
+
7
+ class FastFailure < StandardError; end
8
+
9
+ def self.configure(&block)
10
+ block.call(configuration)
11
+ end
12
+
13
+ def self.configuration
14
+ @configuration ||= CircuitB::Configuration.new
15
+ end
16
+
17
+ def self.reset_configuration
18
+ @configuration = nil
19
+ end
20
+
21
+ def self.fuse(name, &block)
22
+ raise "Fuse with the name '#{name}' is not registered" unless fuse = configuration.fuses[name]
23
+
24
+ if block
25
+ fuse.wrap(&block)
26
+ else
27
+ return fuse
28
+ end
29
+ end
30
+ end
31
+
32
+ def CircuitB(fuse_name, &block)
33
+ CircuitB.fuse(fuse_name, &block)
34
+ end
@@ -0,0 +1,6 @@
1
+ require "rubygems"
2
+ require "test/unit"
3
+ require "shoulda"
4
+ require "timecop"
5
+
6
+ $:.unshift(File.dirname(__FILE__) + "/../lib")
@@ -0,0 +1,39 @@
1
+ require File.dirname(__FILE__) + "/../../test_helper"
2
+ require "circuit_b/configuration"
3
+
4
+ class CircuitB::TestConfiguration < Test::Unit::TestCase
5
+
6
+ def setup
7
+ @config = CircuitB::Configuration.new
8
+ end
9
+
10
+ should "configure memory storage by default" do
11
+ assert @config.state_storage.is_a?(CircuitB::Storage::Memory)
12
+ end
13
+
14
+ should "accept default fuse configuration updates" do
15
+ @config.default_fuse_config = {
16
+ :allowed_failures => 2,
17
+ :cool_off_period => 3 # seconds
18
+ }
19
+ end
20
+
21
+ should "add a named fuse with default configuration" do
22
+ @config.add_fuse "fuse_name"
23
+ assert_equal 1, @config.fuses.size
24
+ end
25
+
26
+ should "add a named fuse with custom configuration" do
27
+ @config.add_fuse "fuse_name", :allowed_failures => 5
28
+ end
29
+
30
+ should "disallow adding fuses with the same name" do
31
+ @config.add_fuse "fuse_name"
32
+ begin
33
+ @config.add_fuse "fuse_name"
34
+ fail "should raise an exception"
35
+ rescue
36
+ # Exception is expected
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,123 @@
1
+ require File.dirname(__FILE__) + "/../../test_helper"
2
+ require "circuit_b"
3
+
4
+ class CircuitB::TestFuse < Test::Unit::TestCase
5
+
6
+ context "initialization" do
7
+ should "not allow nil names" do
8
+ begin
9
+ CircuitB::Fuse.new(nil, nil, {})
10
+ fail "Exception is expected"
11
+ rescue => e
12
+ assert_equal "Name must be specified", e.message
13
+ end
14
+ end
15
+
16
+ should "not allow nil-storages" do
17
+ begin
18
+ CircuitB::Fuse.new("name", nil, {})
19
+ fail "Exception is expected"
20
+ rescue => e
21
+ assert_equal "Storage must be specified", e.message
22
+ end
23
+ end
24
+
25
+ should "disallow storages of the wrong type" do
26
+ begin
27
+ CircuitB::Fuse.new("name", "", nil)
28
+ fail "Exception is expected"
29
+ rescue => e
30
+ assert_equal "Storage must be of CircuitB::Storage::Base kind", e.message
31
+ end
32
+ end
33
+
34
+ should "not allow nil-configs" do
35
+ begin
36
+ CircuitB::Fuse.new("name", CircuitB::Storage::Memory.new, nil)
37
+ fail "Exception is expected"
38
+ rescue => e
39
+ assert_equal "Config must be specified", e.message
40
+ end
41
+ end
42
+ end
43
+
44
+ context "operation" do
45
+ setup do
46
+ @fuse = CircuitB::Fuse.new("name", CircuitB::Storage::Memory.new, :allowed_failures => 1, :cool_off_period => 60)
47
+ end
48
+
49
+ should "open when the allowed failures reached" do
50
+ assert !@fuse.open?
51
+ do_failure(@fuse)
52
+ assert @fuse.open?
53
+ end
54
+
55
+ should "reset the failures counter when the attempt succeeds" do
56
+ @fuse = CircuitB::Fuse.new("name", CircuitB::Storage::Memory.new, :allowed_failures => 2)
57
+
58
+ do_failure(@fuse)
59
+ assert_equal 1, @fuse.failures
60
+
61
+ @fuse.wrap do
62
+ # Successful code
63
+ end
64
+
65
+ assert_equal 0, @fuse.failures
66
+ end
67
+
68
+ should "fail fast when open" do
69
+ # Open the fuse and verify it's open
70
+ do_failure(@fuse)
71
+ assert @fuse.open?
72
+
73
+ begin
74
+ @fuse.wrap do
75
+ fail "Must not execute as fail-fast exception is expected"
76
+ end
77
+ rescue => e
78
+ assert e.is_a?(CircuitB::FastFailure), "Wrong exception: #{e.inspect}"
79
+ end
80
+ end
81
+
82
+ should "close after the cooling period" do
83
+ do_failure(@fuse)
84
+
85
+ Timecop.travel(Time.now + @fuse.config[:cool_off_period] + 1) do
86
+ @fuse.send(:close_if_cooled_off)
87
+
88
+ assert !@fuse.open?
89
+ assert_equal 0, @fuse.failures
90
+ end
91
+ end
92
+
93
+ should "not count fast failure as an error" do
94
+ do_failure(@fuse)
95
+
96
+ # Get the fast failure
97
+ Timecop.travel(Time.now + @fuse.config[:cool_off_period] / 2) do
98
+ begin
99
+ do_failure(@fuse, true)
100
+ fail "Fast failure is expected"
101
+ rescue CircuitB::FastFailure => e
102
+ # Expected
103
+ end
104
+ end
105
+
106
+ # The above fast failure should not affect the cooling off schedule
107
+ Timecop.travel(Time.now + @fuse.config[:cool_off_period] + 1) do
108
+ @fuse.send(:close_if_cooled_off)
109
+ assert !@fuse.open?
110
+ end
111
+ end
112
+ end
113
+
114
+ def do_failure(fuse, rethrow = false)
115
+ begin
116
+ @fuse.wrap do
117
+ raise "Exceptional code"
118
+ end
119
+ rescue => e
120
+ raise e if rethrow
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,77 @@
1
+ require File.dirname(__FILE__) + "/../test_helper"
2
+ require "circuit_b"
3
+ require "circuit_b/configuration"
4
+ require "circuit_b/storage"
5
+
6
+ class TestCircuitB < Test::Unit::TestCase
7
+
8
+ context "configuration" do
9
+ should "accept configuration paramters" do
10
+ CircuitB.configure do |config|
11
+ config.state_storage = CircuitB::Storage::Memory.new
12
+
13
+ config.default_fuse_config = {
14
+ :allowed_failures => 2,
15
+ :cool_off_period => 3 # seconds
16
+ }
17
+
18
+ config.add_fuse("mail", {
19
+ :allowed_failures => 5,
20
+ :cool_off_period => 10 # seconds
21
+ })
22
+ end
23
+ end
24
+
25
+ should "return configuration" do
26
+ config = CircuitB.configuration
27
+ assert config.is_a?(CircuitB::Configuration)
28
+ end
29
+ end
30
+
31
+ context "using fuses to protect code" do
32
+ setup do
33
+ CircuitB.reset_configuration
34
+ CircuitB.configure do |c|
35
+ c.state_storage = CircuitB::Storage::Redis.new
36
+ c.add_fuse "fuse_name", :allowed_failures => 1, :cool_off_period => 10
37
+ end
38
+ end
39
+
40
+ should "let wrap the code with fuse" do
41
+ executed = false
42
+ CircuitB("fuse_name") do
43
+ # Some lengthy and potentially failing operation
44
+ executed = true
45
+ end
46
+
47
+ assert executed, "Code wasn't executed"
48
+ end
49
+
50
+ should "error out if the fuse doesn't exist" do
51
+ begin
52
+ CircuitB("non_existent_fuse") do
53
+ # Will never be executed
54
+ fail "Should never be executed"
55
+ end
56
+ rescue => e
57
+ assert_equal "Fuse with the name 'non_existent_fuse' is not registered", e.message
58
+ end
59
+ end
60
+
61
+ should "pass the error when it's raised by the code" do
62
+ begin
63
+ CircuitB("fuse_name") do
64
+ raise "App error"
65
+ end
66
+ fail "App error should be raised"
67
+ rescue => e
68
+ assert_equal "App error", e.message
69
+ end
70
+ end
71
+
72
+ should "return the fuse if no block is given" do
73
+ assert CircuitB("fuse_name").is_a?(CircuitB::Fuse)
74
+ end
75
+ end
76
+
77
+ end
metadata ADDED
@@ -0,0 +1,69 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: circuit_b
3
+ version: !ruby/object:Gem::Version
4
+ version: "1.0"
5
+ platform: ruby
6
+ authors:
7
+ - Aleksey Gureiev
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2010-01-30 00:00:00 +11:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: Classic circuit breaker to protect resources from being accessed over and over while in pain.
17
+ email: spyromus@noizeramp.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files:
23
+ - MIT-LICENSE
24
+ - README.md
25
+ files:
26
+ - lib/circuit_b/configuration.rb
27
+ - lib/circuit_b/fuse.rb
28
+ - lib/circuit_b/storage/base.rb
29
+ - lib/circuit_b/storage/memory.rb
30
+ - lib/circuit_b/storage/redis.rb
31
+ - lib/circuit_b/storage.rb
32
+ - lib/circuit_b.rb
33
+ - test/test_helper.rb
34
+ - test/unit/circuit_b/test_configuration.rb
35
+ - test/unit/circuit_b/test_fuse.rb
36
+ - test/unit/test_circuit_b.rb
37
+ - MIT-LICENSE
38
+ - README.md
39
+ has_rdoc: true
40
+ homepage: http://github.com/alg/circuit_b
41
+ licenses: []
42
+
43
+ post_install_message:
44
+ rdoc_options:
45
+ - --title
46
+ - CircuitB - Distributed circuit breaker
47
+ require_paths:
48
+ - lib
49
+ required_ruby_version: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: "0"
54
+ version:
55
+ required_rubygems_version: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: "0"
60
+ version:
61
+ requirements: []
62
+
63
+ rubyforge_project:
64
+ rubygems_version: 1.3.5
65
+ signing_key:
66
+ specification_version: 3
67
+ summary: Distributed circuit breaker
68
+ test_files: []
69
+