circuit_b 1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +7 -0
- data/README.md +104 -0
- data/lib/circuit_b/configuration.rb +34 -0
- data/lib/circuit_b/fuse.rb +74 -0
- data/lib/circuit_b/storage/base.rb +18 -0
- data/lib/circuit_b/storage/memory.rb +27 -0
- data/lib/circuit_b/storage/redis.rb +31 -0
- data/lib/circuit_b/storage.rb +2 -0
- data/lib/circuit_b.rb +34 -0
- data/test/test_helper.rb +6 -0
- data/test/unit/circuit_b/test_configuration.rb +39 -0
- data/test/unit/circuit_b/test_fuse.rb +123 -0
- data/test/unit/test_circuit_b.rb +77 -0
- metadata +69 -0
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
|
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
|
data/test/test_helper.rb
ADDED
@@ -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
|
+
|