disyuntor 0.1.0 → 0.9.0
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.
- checksums.yaml +4 -4
- data/.travis.yml +9 -0
- data/README.md +14 -22
- data/disyuntor.gemspec +1 -1
- data/lib/disyuntor.rb +40 -56
- data/lib/disyuntor/version.rb +1 -1
- data/test/disyuntor_test.rb +186 -0
- metadata +11 -10
- data/lib/rack/disyuntor.rb +0 -20
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 601f1ae1935ac87a0fffe20e3f3181dd4cafc5c6
|
4
|
+
data.tar.gz: 7a6815a58f579049f49c018796cfe45a51763239
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: dbd2b6f2d21f9091320eeb45b1c7e02d0dab9c576dac0575acfee8abeb8519d1af7cec61170a0055272d387044c1764863387e88803413b1b7452f6accb1c19f
|
7
|
+
data.tar.gz: 3d1d685bac30efb63cb3f6208ff46a02a5c2667b356d293b9a871a6f17dd5532bdc6358220b7c40c8154a7f7497de45ebc2ac5365fe4e6daa76f00fb05a4b3ac
|
data/.travis.yml
ADDED
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# Simple Circuit Breaker Pattern in Ruby
|
2
2
|
|
3
|
-
[](https://gitter.im/inkel/disyuntor?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
3
|
+
[](https://gitter.im/inkel/disyuntor?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 
|
4
4
|
|
5
5
|
This gem implements a very simple class to deal with the Circuit Breaker Pattern as described by [Michael T. Nygard](http://www.michaelnygard.com/) in his amazing and highly recommended [Release It! - Design and Deploy Production-Ready Software](http://www.amazon.com/Release-It-Production-Ready-Pragmatic-Programmers/dp/0978739213).
|
6
6
|
|
@@ -9,41 +9,33 @@ This gem implements a very simple class to deal with the Circuit Breaker Pattern
|
|
9
9
|
```ruby
|
10
10
|
require "disyuntor"
|
11
11
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
# Wait 5 seconds before trying again
|
16
|
-
timeout: 5
|
17
|
-
}
|
12
|
+
# Trip circuit after 10 errors
|
13
|
+
# Wait 5 seconds before trying again
|
14
|
+
disyuntor = Disyuntor.new(threshold: 10, timeout: 5)
|
18
15
|
|
19
|
-
|
20
|
-
|
21
|
-
res = circuit_breaker.try do
|
16
|
+
res = disyuntor.try do
|
22
17
|
# …your potentially failing operation…
|
23
18
|
end
|
24
19
|
```
|
25
20
|
|
26
|
-
|
27
|
-
|
28
|
-
If you want to use it as a [`Rack`](https://github.com/rack/rack) middleware, add the following in your `config.ru`:
|
29
|
-
|
30
|
-
```ruby
|
31
|
-
require "rack/disyuntor"
|
21
|
+
A _Disyuntor_, or circuit breaker, has two (and a half) possible states:
|
32
22
|
|
33
|
-
|
34
|
-
|
23
|
+
* `#closed?` for when the protection hasn't detected any issues and your code is allowed to run;
|
24
|
+
* `#open?` for when the protection has reached a `threshold` of issues and your code won't be allowed to run.
|
35
25
|
|
36
|
-
|
26
|
+
The third, or rather second and a half state, is for when the circuit was open and `timeout` seconds passed. In this state your code is allowed to run **just once**. If it works without raising any new failure, then the circuit will automatically close itself until, otherwise it will remain in an open state for a new `timeout` interval in seconds.
|
37
27
|
|
38
28
|
## Custom actions when circuit is open
|
39
29
|
|
30
|
+
By default, when the circuit is open, `Disyuntor#try` will fail with a `Disyuntor::CircuitOpenError`. This behavior can be changed by passing a `Proc` in the `on_circuit_open` option or method.
|
31
|
+
|
40
32
|
Every time the circuit is open, the `#on_circuit_open` method is called, passing the circuit as its argument. This allows customizing the failure mode of your circuit:
|
41
33
|
|
42
34
|
```ruby
|
43
|
-
|
35
|
+
disyuntor = Disyuntor.new(threshold: 3, timeout: 5)
|
44
36
|
|
45
|
-
|
46
|
-
"
|
37
|
+
disyuntor.on_circuit_open do |c|
|
38
|
+
puts "Ooops, can't execute circuit"
|
47
39
|
end
|
48
40
|
```
|
49
41
|
|
data/disyuntor.gemspec
CHANGED
data/lib/disyuntor.rb
CHANGED
@@ -1,92 +1,76 @@
|
|
1
|
-
require "micromachine"
|
2
|
-
|
3
1
|
class Disyuntor
|
4
2
|
CircuitOpenError = Class.new(RuntimeError)
|
5
3
|
|
6
|
-
attr_reader :failures, :
|
4
|
+
attr_reader :failures, :threshold, :timeout
|
7
5
|
|
8
|
-
def initialize(threshold
|
6
|
+
def initialize(threshold:, timeout:)
|
9
7
|
@threshold = threshold
|
10
8
|
@timeout = timeout
|
11
9
|
|
12
|
-
|
13
|
-
block
|
14
|
-
else
|
15
|
-
Proc.new{ fail CircuitOpenError }
|
16
|
-
end
|
10
|
+
on_circuit_open { fail CircuitOpenError }
|
17
11
|
|
18
12
|
close!
|
19
13
|
end
|
20
14
|
|
21
|
-
def
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
15
|
+
def try(&block)
|
16
|
+
if closed? or timed_out?
|
17
|
+
circuit_closed(&block)
|
18
|
+
else
|
19
|
+
circuit_open
|
20
|
+
end
|
21
|
+
end
|
26
22
|
|
27
|
-
|
28
|
-
|
29
|
-
|
23
|
+
def on_circuit_open(&block)
|
24
|
+
raise ArgumentError, "Must pass a block" unless block_given?
|
25
|
+
@on_circuit_open = block
|
26
|
+
end
|
30
27
|
|
31
|
-
|
32
|
-
|
33
|
-
@failures = 0
|
34
|
-
end
|
35
|
-
end
|
28
|
+
def closed?
|
29
|
+
state == :closed
|
36
30
|
end
|
37
31
|
|
38
|
-
def
|
39
|
-
|
40
|
-
|
32
|
+
def open?
|
33
|
+
not (closed? or timed_out?)
|
34
|
+
end
|
41
35
|
|
42
|
-
|
36
|
+
private
|
43
37
|
|
44
|
-
|
45
|
-
def open? () state == :open end
|
46
|
-
def half_open? () state == :half_open end
|
38
|
+
attr_reader :opened_at, :state
|
47
39
|
|
48
|
-
def
|
49
|
-
|
40
|
+
def close!
|
41
|
+
@opened_at = nil
|
42
|
+
@failures = 0
|
43
|
+
@state = :closed
|
50
44
|
end
|
51
45
|
|
52
|
-
def
|
53
|
-
|
46
|
+
def open!
|
47
|
+
@opened_at = Time.now.to_i
|
48
|
+
@state = :open
|
49
|
+
end
|
54
50
|
|
55
|
-
|
56
|
-
|
57
|
-
when half_open? then on_circuit_half_open(&block)
|
58
|
-
when open? then on_circuit_open
|
59
|
-
else
|
60
|
-
fail RuntimeError, "Invalid state! #{state}"
|
61
|
-
end
|
51
|
+
def timed_out?
|
52
|
+
Time.now.to_i > next_timeout_at
|
62
53
|
end
|
63
54
|
|
64
|
-
def
|
65
|
-
|
66
|
-
|
55
|
+
def next_timeout_at
|
56
|
+
opened_at + timeout
|
57
|
+
end
|
58
|
+
|
59
|
+
def increment_failures!
|
67
60
|
@failures += 1
|
68
|
-
open! if @failures > @threshold
|
69
|
-
raise
|
70
|
-
else
|
71
|
-
close!
|
72
|
-
ret
|
73
61
|
end
|
74
62
|
|
75
|
-
def
|
63
|
+
def circuit_closed(&block)
|
76
64
|
ret = block.call
|
77
65
|
rescue
|
78
|
-
open!
|
66
|
+
open! if increment_failures! >= threshold
|
79
67
|
raise
|
80
68
|
else
|
81
69
|
close!
|
82
70
|
ret
|
83
71
|
end
|
84
72
|
|
85
|
-
def
|
86
|
-
|
87
|
-
@on_circuit_open = block
|
88
|
-
else
|
89
|
-
@on_circuit_open.(self)
|
90
|
-
end
|
73
|
+
def circuit_open
|
74
|
+
@on_circuit_open.call(self)
|
91
75
|
end
|
92
76
|
end
|
data/lib/disyuntor/version.rb
CHANGED
@@ -0,0 +1,186 @@
|
|
1
|
+
if ENV["COVERAGE"]
|
2
|
+
require "simplecov"
|
3
|
+
SimpleCov.start do
|
4
|
+
add_filter "/test/"
|
5
|
+
add_filter "/spec/"
|
6
|
+
add_filter "/.gs/"
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
require "minitest/autorun"
|
11
|
+
require_relative "../lib/disyuntor"
|
12
|
+
|
13
|
+
class DisyuntorTest < Minitest::Test
|
14
|
+
CustomRuntimeError = Class.new(RuntimeError)
|
15
|
+
|
16
|
+
def test_initialize_without_defaults
|
17
|
+
assert_raises(ArgumentError) { Disyuntor.new }
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_initialize_requires_timeout
|
21
|
+
assert_raises(ArgumentError) do
|
22
|
+
Disyuntor.new(threshold: 1)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_initialize_requires_threshold
|
27
|
+
assert_raises(ArgumentError) do
|
28
|
+
Disyuntor.new(timeout: 1)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def setup
|
33
|
+
@threshold = 3
|
34
|
+
@timeout = 5
|
35
|
+
@disyuntor = Disyuntor.new(threshold: @threshold, timeout: @timeout)
|
36
|
+
end
|
37
|
+
|
38
|
+
def test_initialize_closed
|
39
|
+
assert @disyuntor.closed?
|
40
|
+
end
|
41
|
+
|
42
|
+
def test_initialize_without_failures
|
43
|
+
assert_equal 0, @disyuntor.failures
|
44
|
+
end
|
45
|
+
|
46
|
+
def test_closed_circuit_returns_block_value
|
47
|
+
assert_equal 42, @disyuntor.try { 42 }
|
48
|
+
end
|
49
|
+
|
50
|
+
def test_closed_circuit_do_not_count_failures_on_success
|
51
|
+
@disyuntor.try { true }
|
52
|
+
assert_equal 0, @disyuntor.failures
|
53
|
+
end
|
54
|
+
|
55
|
+
def test_reset_failures_counter_on_closed_circuit_success
|
56
|
+
begin
|
57
|
+
@disyuntor.try { fail CustomRuntimeError }
|
58
|
+
rescue CustomRuntimeError
|
59
|
+
end
|
60
|
+
|
61
|
+
assert_equal 1, @disyuntor.failures
|
62
|
+
|
63
|
+
@disyuntor.try { true }
|
64
|
+
|
65
|
+
assert_equal 0, @disyuntor.failures
|
66
|
+
end
|
67
|
+
|
68
|
+
def make_open(breaker)
|
69
|
+
breaker.threshold.times do
|
70
|
+
begin
|
71
|
+
breaker.try { fail CustomRuntimeError }
|
72
|
+
rescue CustomRuntimeError
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def after_timeout(breaker, &block)
|
78
|
+
Time.stub(:now, Time.at(Time.now.to_i + breaker.timeout + 1), &block)
|
79
|
+
end
|
80
|
+
|
81
|
+
def test_open_circuit_after_threshold_failures
|
82
|
+
@disyuntor.threshold.times do
|
83
|
+
begin
|
84
|
+
@disyuntor.try { fail CustomRuntimeError }
|
85
|
+
rescue CustomRuntimeError
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
refute @disyuntor.closed?
|
90
|
+
end
|
91
|
+
|
92
|
+
def test_open_circuit_raises_default_error
|
93
|
+
make_open(@disyuntor)
|
94
|
+
|
95
|
+
assert_raises(Disyuntor::CircuitOpenError) do
|
96
|
+
@disyuntor.try { fail CustomRuntimeError }
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def test_override_on_circuit_open
|
101
|
+
@disyuntor.on_circuit_open { 42 }
|
102
|
+
|
103
|
+
make_open(@disyuntor)
|
104
|
+
|
105
|
+
assert_equal 42, @disyuntor.try { fail CustomRuntimeError }
|
106
|
+
end
|
107
|
+
|
108
|
+
def test_count_failures
|
109
|
+
assert_equal 0, @disyuntor.failures
|
110
|
+
|
111
|
+
make_open(@disyuntor)
|
112
|
+
|
113
|
+
assert_equal @threshold, @disyuntor.failures
|
114
|
+
end
|
115
|
+
|
116
|
+
def test_do_not_count_failures_when_open
|
117
|
+
make_open(@disyuntor)
|
118
|
+
|
119
|
+
begin
|
120
|
+
@disyuntor.try { fail CustomRuntimeError }
|
121
|
+
rescue Disyuntor::CircuitOpenError
|
122
|
+
end
|
123
|
+
|
124
|
+
assert_equal @threshold, @disyuntor.failures
|
125
|
+
end
|
126
|
+
|
127
|
+
def test_close_after_timeout_if_success
|
128
|
+
make_open(@disyuntor)
|
129
|
+
|
130
|
+
refute @disyuntor.closed?
|
131
|
+
|
132
|
+
after_timeout(@disyuntor) do
|
133
|
+
assert_equal 42, @disyuntor.try { 42 }
|
134
|
+
end
|
135
|
+
|
136
|
+
assert @disyuntor.closed?
|
137
|
+
end
|
138
|
+
|
139
|
+
def test_reopen_after_timeout_if_fails
|
140
|
+
make_open(@disyuntor)
|
141
|
+
|
142
|
+
refute @disyuntor.closed?
|
143
|
+
|
144
|
+
after_timeout(@disyuntor) do
|
145
|
+
assert_raises(CustomRuntimeError) do
|
146
|
+
@disyuntor.try { fail CustomRuntimeError }
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
refute @disyuntor.closed?
|
151
|
+
end
|
152
|
+
|
153
|
+
def test_count_failure_after_timeout_if_fails
|
154
|
+
make_open(@disyuntor)
|
155
|
+
|
156
|
+
refute @disyuntor.closed?
|
157
|
+
|
158
|
+
after_timeout(@disyuntor) do
|
159
|
+
assert_raises(CustomRuntimeError) do
|
160
|
+
@disyuntor.try { fail CustomRuntimeError }
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
assert_equal @threshold.next, @disyuntor.failures
|
165
|
+
end
|
166
|
+
|
167
|
+
def test_close_after_timeout_if_succeeds
|
168
|
+
make_open(@disyuntor)
|
169
|
+
|
170
|
+
refute @disyuntor.closed?
|
171
|
+
|
172
|
+
after_timeout(@disyuntor) do
|
173
|
+
assert_equal 42, @disyuntor.try { 42 }
|
174
|
+
end
|
175
|
+
|
176
|
+
assert @disyuntor.closed?
|
177
|
+
end
|
178
|
+
|
179
|
+
def test_do_not_report_open_when_timed_out
|
180
|
+
make_open(@disyuntor)
|
181
|
+
|
182
|
+
after_timeout(@disyuntor) do
|
183
|
+
refute @disyuntor.open?
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
metadata
CHANGED
@@ -1,29 +1,29 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: disyuntor
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.9.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Leandro López
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2016-06-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
14
|
+
name: minitest
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- -
|
17
|
+
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version:
|
20
|
-
type: :
|
19
|
+
version: 5.8.4
|
20
|
+
type: :development
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- -
|
24
|
+
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version:
|
26
|
+
version: 5.8.4
|
27
27
|
description: Simple implementation of Michael T. Nygard's Circuit Breaker Pattern
|
28
28
|
email:
|
29
29
|
- inkel.ar@gmail.com
|
@@ -31,12 +31,13 @@ executables: []
|
|
31
31
|
extensions: []
|
32
32
|
extra_rdoc_files: []
|
33
33
|
files:
|
34
|
+
- ".travis.yml"
|
34
35
|
- LICENSE
|
35
36
|
- README.md
|
36
37
|
- disyuntor.gemspec
|
37
38
|
- lib/disyuntor.rb
|
38
39
|
- lib/disyuntor/version.rb
|
39
|
-
-
|
40
|
+
- test/disyuntor_test.rb
|
40
41
|
homepage: http://github.com/inkel/disyuntor
|
41
42
|
licenses:
|
42
43
|
- MIT
|
@@ -57,7 +58,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
57
58
|
version: '0'
|
58
59
|
requirements: []
|
59
60
|
rubyforge_project:
|
60
|
-
rubygems_version: 2.
|
61
|
+
rubygems_version: 2.4.5.1
|
61
62
|
signing_key:
|
62
63
|
specification_version: 4
|
63
64
|
summary: Circuit Breaker Pattern in Ruby
|
data/lib/rack/disyuntor.rb
DELETED
@@ -1,20 +0,0 @@
|
|
1
|
-
require "rack"
|
2
|
-
require_relative "../disyuntor"
|
3
|
-
|
4
|
-
class Rack::Disyuntor
|
5
|
-
def initialize(app, options={})
|
6
|
-
@app = app
|
7
|
-
|
8
|
-
options[:on_circuit_open] ||= -> { circuit_open_response }
|
9
|
-
|
10
|
-
@circuit_breaker = Disyuntor.new(options)
|
11
|
-
end
|
12
|
-
|
13
|
-
def call(env)
|
14
|
-
@circuit_breaker.try { @app.call(env) }
|
15
|
-
end
|
16
|
-
|
17
|
-
def circuit_open_response
|
18
|
-
[503, { "Content-Type" => "text/plain" }, ["Service Unavailable"]]
|
19
|
-
end
|
20
|
-
end
|