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