disyuntor 0.1.0 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 7ab5e6badbc47e7babf6bd794c93b14c826458bd
4
- data.tar.gz: a8b0a6b6fc72713b987ae48fb091364471568d08
3
+ metadata.gz: 601f1ae1935ac87a0fffe20e3f3181dd4cafc5c6
4
+ data.tar.gz: 7a6815a58f579049f49c018796cfe45a51763239
5
5
  SHA512:
6
- metadata.gz: 3bd3266ebe4e42789dbeac531495e08544a2959ed4b62b39e900a312f6cb1084bbab73f8f02d21aac1df44303d614f077ddb8324642187f9f866e84a31ef2264
7
- data.tar.gz: 5d2350086bb03289c4510998d7fa806fd3e7c443c7b2615f77750432a8e2304c37dda443f6d8454a8470d2942981d8ddecfce77131e0dfa5c5f809d083137046
6
+ metadata.gz: dbd2b6f2d21f9091320eeb45b1c7e02d0dab9c576dac0575acfee8abeb8519d1af7cec61170a0055272d387044c1764863387e88803413b1b7452f6accb1c19f
7
+ data.tar.gz: 3d1d685bac30efb63cb3f6208ff46a02a5c2667b356d293b9a871a6f17dd5532bdc6358220b7c40c8154a7f7497de45ebc2ac5365fe4e6daa76f00fb05a4b3ac
@@ -0,0 +1,9 @@
1
+ language: ruby
2
+
3
+ install: gem install minitest:5.8.4
4
+
5
+ rvm:
6
+ - "2.1"
7
+ - "2.2"
8
+
9
+ script: ruby test/disyuntor_test.rb -v
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Simple Circuit Breaker Pattern in Ruby
2
2
 
3
- [![Join the chat at https://gitter.im/inkel/disyuntor](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/inkel/disyuntor?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
3
+ [![Join the chat at https://gitter.im/inkel/disyuntor](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/inkel/disyuntor?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) ![build status on master](https://travis-ci.org/inkel/disyuntor.svg?branch=master)
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
- options = {
13
- # Trip circuit after 10 errors
14
- threshold: 10,
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
- circuit_breaker = Disyuntor.new(options)
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
- 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.
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
- use Rack::Disyuntor, threshold: 10, timeout: 5
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
- This will start responding with `[503, { "Content-Type" => "text/plain", ["Service Unavailable"]]` when the circuit is open.
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
- circuit_breaker = Disyuntor.new(threshold: 3, timeout: 5)
35
+ disyuntor = Disyuntor.new(threshold: 3, timeout: 5)
44
36
 
45
- circuit_breaker.on_circuit_open do |c|
46
- "Circuit was open at #{c.opened_at}"
37
+ disyuntor.on_circuit_open do |c|
38
+ puts "Ooops, can't execute circuit"
47
39
  end
48
40
  ```
49
41
 
@@ -16,5 +16,5 @@ Gem::Specification.new do |s|
16
16
 
17
17
  s.files = `git ls-files`.split("\n")
18
18
 
19
- s.add_dependency "micromachine", "1.2.0"
19
+ s.add_development_dependency "minitest", ">= 5.8.4"
20
20
  end
@@ -1,92 +1,76 @@
1
- require "micromachine"
2
-
3
1
  class Disyuntor
4
2
  CircuitOpenError = Class.new(RuntimeError)
5
3
 
6
- attr_reader :failures, :opened_at, :threshold, :timeout
4
+ attr_reader :failures, :threshold, :timeout
7
5
 
8
- def initialize(threshold: 5, timeout: 10, &block)
6
+ def initialize(threshold:, timeout:)
9
7
  @threshold = threshold
10
8
  @timeout = timeout
11
9
 
12
- @on_circuit_open = if block_given?
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 states
22
- @states ||= MicroMachine.new(:closed).tap do |fsm|
23
- fsm.when(:trip, :half_open => :open, :closed => :open)
24
- fsm.when(:reset, :half_open => :closed, :closed => :closed)
25
- fsm.when(:try, :open => :half_open)
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
- fsm.on(:open) do
28
- @opened_at = Time.now.to_i
29
- end
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
- fsm.on(:closed) do
32
- @opened_at = nil
33
- @failures = 0
34
- end
35
- end
28
+ def closed?
29
+ state == :closed
36
30
  end
37
31
 
38
- def close! () states.trigger!(:reset) end
39
- def open! () states.trigger!(:trip) end
40
- def half_open! () states.trigger!(:try) end
32
+ def open?
33
+ not (closed? or timed_out?)
34
+ end
41
35
 
42
- def state () states.state end
36
+ private
43
37
 
44
- def closed? () state == :closed end
45
- def open? () state == :open end
46
- def half_open? () state == :half_open end
38
+ attr_reader :opened_at, :state
47
39
 
48
- def timed_out?
49
- open? && Time.now.to_i > (@opened_at + @timeout)
40
+ def close!
41
+ @opened_at = nil
42
+ @failures = 0
43
+ @state = :closed
50
44
  end
51
45
 
52
- def try(&block)
53
- half_open! if timed_out?
46
+ def open!
47
+ @opened_at = Time.now.to_i
48
+ @state = :open
49
+ end
54
50
 
55
- case
56
- when closed? then on_circuit_closed(&block)
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 on_circuit_closed(&block)
65
- ret = block.call
66
- rescue
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 on_circuit_half_open(&block)
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 on_circuit_open(&block)
86
- if block_given?
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
@@ -1,3 +1,3 @@
1
1
  class Disyuntor
2
- VERSION = "0.1.0"
2
+ VERSION = "0.9.0"
3
3
  end
@@ -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.1.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: 2015-09-22 00:00:00.000000000 Z
11
+ date: 2016-06-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: micromachine
14
+ name: minitest
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - '='
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 1.2.0
20
- type: :runtime
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: 1.2.0
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
- - lib/rack/disyuntor.rb
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.2.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
@@ -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