breaker 0.1.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 +7 -0
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +96 -0
- data/Rakefile +9 -0
- data/breaker.gemspec +23 -0
- data/lib/breaker.rb +133 -0
- data/lib/breaker/in_memory_repo.rb +69 -0
- data/lib/breaker/test_cases.rb +159 -0
- data/lib/breaker/version.rb +3 -0
- data/test/acceptance_test.rb +16 -0
- data/test/test_helper.rb +5 -0
- metadata +86 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: e96a4a527f8764f01191e363daf25d59bcfc44a1
|
4
|
+
data.tar.gz: 9707df9e63bbdce0166b471beab29456f67c5c6b
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 43cb66bf520aa9d6abd763b274b195a2a64387215c7c31799430f576a742f602dc8ac466edd3b88bcb948d37ade1b5a0f9ac98559ae2673afa9dc1358f4e1dbc
|
7
|
+
data.tar.gz: 4436478a597986c372c032ac72bf0dea6977a2b34b27cfc294113746f6bb6ef4bdf1d454c47d90a794364c57d1421c172b25a218a1e1ae86345bc4a3ebdc0715
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 ahawkins
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
# Breaker
|
2
|
+
|
3
|
+
Circuit Breakers for well designed applications.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'breaker'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install breaker
|
18
|
+
|
19
|
+
## Circuit Breaker Pattern
|
20
|
+
|
21
|
+
The circuit breaker pattern is described in the (wonderful) book
|
22
|
+
[Release It](http://pragprog.com/book/mnee/release-it) by Michael T.
|
23
|
+
Nygard. A circuit breaker in programming terms is modeled after a
|
24
|
+
circuit breaker in the real world. A circuit breaker protects the
|
25
|
+
larger system from failures in other systems. They are especially
|
26
|
+
useful for protecting an application from finnicky remote systems.
|
27
|
+
|
28
|
+
The circuit is a state machine. It has three states: open, closed, and
|
29
|
+
half-open. The circuit starts off in `open` state--normal operation.
|
30
|
+
If the operation failures N times (the failure threshold) the circuit
|
31
|
+
moves to `closed`. Calls in the `closed` state will immediate fail and
|
32
|
+
raise an exception. After a specified time period has passed (retry
|
33
|
+
timeout) the circuit moves into `half-open`. Calls happen normally. If
|
34
|
+
a call fails the state moves to `open`. If the call suceeds it moves
|
35
|
+
to `open`. All calls are capped with a timeout. If a timeout occurs
|
36
|
+
that counts as a failure.
|
37
|
+
|
38
|
+
## Usage
|
39
|
+
|
40
|
+
Most interaction should go through `Breaker.circuit`. This is a
|
41
|
+
factory method that creates `Breaker::Circuit` objects. It requires
|
42
|
+
one argument: the name. It also takes an `options` hash for
|
43
|
+
customizing the circuit. Thirdly it takes a block to run inside the
|
44
|
+
circuit. Here are some examples:
|
45
|
+
|
46
|
+
```ruby
|
47
|
+
# Simplest example: protect some code with a circuit breaker
|
48
|
+
Breaker.circuit 'twitter' do
|
49
|
+
Tweet.post 'Oh Hai'
|
50
|
+
end
|
51
|
+
```
|
52
|
+
|
53
|
+
`Breaker.circuit` is an upsert opertion. It will create or update an
|
54
|
+
existing circuit. Pass the options hash to customize the circuit's
|
55
|
+
behavior.
|
56
|
+
|
57
|
+
```ruby
|
58
|
+
circuit = Breaker.circuit 'twitter', timeout: 5
|
59
|
+
circuit.run do
|
60
|
+
Tweet.post 'Oh Hai'
|
61
|
+
end
|
62
|
+
```
|
63
|
+
|
64
|
+
`Breaker.circuit` returns `Breaker::Circuit` instances which can be
|
65
|
+
saved for later. There use depends no how persistence works.
|
66
|
+
|
67
|
+
## Persistence
|
68
|
+
|
69
|
+
Circuit breakers are only really useful in a large system (perhaps
|
70
|
+
distributed). Some information must be shared acrosss subsystems. Say
|
71
|
+
there are 5 different services in the system. Each is
|
72
|
+
talking to 2 different systems protected by circuit breakers. Either
|
73
|
+
of the systems go down. It's natural that the failure should propogate
|
74
|
+
through the system so that each service knows the shared ones are
|
75
|
+
down. This is where state and persistence come into play.
|
76
|
+
|
77
|
+
The breaker gems bundles a simple in memory repository. This is
|
78
|
+
process specific. If you need to share state across multiple processes
|
79
|
+
then you must write your own repository.
|
80
|
+
|
81
|
+
The repository manages fueses (in the eletrical sense). A fuse
|
82
|
+
maintains state. The repository must implement one method: `upsert`
|
83
|
+
which creates or updates a fuse given by name. The repistory can
|
84
|
+
return some sort of persistent fuse where writer methods write to
|
85
|
+
persistent storage.
|
86
|
+
|
87
|
+
Refer to `lib/breaker/in_memory_repo.rb` for an example. The class is
|
88
|
+
very simple.
|
89
|
+
|
90
|
+
## Contributing
|
91
|
+
|
92
|
+
1. Fork it
|
93
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
94
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
95
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
96
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
data/breaker.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'breaker/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "breaker"
|
8
|
+
spec.version = Breaker::VERSION
|
9
|
+
spec.authors = ["ahawkins"]
|
10
|
+
spec.email = ["adam@hawkins.io"]
|
11
|
+
spec.description = %q{Circuit breaker pattern for well designed Ruby applications }
|
12
|
+
spec.summary = %q{}
|
13
|
+
spec.homepage = "https://github.com/ahawkins/breaker"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
22
|
+
spec.add_development_dependency "rake"
|
23
|
+
end
|
data/lib/breaker.rb
ADDED
@@ -0,0 +1,133 @@
|
|
1
|
+
require "breaker/version"
|
2
|
+
require 'timeout'
|
3
|
+
|
4
|
+
module Breaker
|
5
|
+
CircuitOpenError = Class.new RuntimeError
|
6
|
+
|
7
|
+
class << self
|
8
|
+
def circuit(name, options = {})
|
9
|
+
fuse = repo.upsert options.merge(name: name)
|
10
|
+
|
11
|
+
circuit = Circuit.new fuse
|
12
|
+
|
13
|
+
if block_given?
|
14
|
+
circuit.run do
|
15
|
+
yield
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
circuit
|
20
|
+
end
|
21
|
+
|
22
|
+
def closed?(name)
|
23
|
+
circuit(name).closed?
|
24
|
+
end
|
25
|
+
alias up? closed?
|
26
|
+
|
27
|
+
def open?(name)
|
28
|
+
circuit(name).open?
|
29
|
+
end
|
30
|
+
alias down? open?
|
31
|
+
|
32
|
+
def repo
|
33
|
+
@repo
|
34
|
+
end
|
35
|
+
|
36
|
+
def repo=(repo)
|
37
|
+
@repo = repo
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
class Circuit
|
42
|
+
attr_accessor :fuse
|
43
|
+
|
44
|
+
def initialize(fuse)
|
45
|
+
@fuse = fuse
|
46
|
+
end
|
47
|
+
|
48
|
+
def name
|
49
|
+
fuse.name
|
50
|
+
end
|
51
|
+
|
52
|
+
def open(clock = Time.now)
|
53
|
+
fuse.failure_count = 1
|
54
|
+
fuse.state = :open
|
55
|
+
fuse.retry_threshold = clock + retry_timeout
|
56
|
+
end
|
57
|
+
|
58
|
+
def close
|
59
|
+
fuse.failure_count = 0
|
60
|
+
fuse.state = :closed
|
61
|
+
fuse.retry_threshold = nil
|
62
|
+
end
|
63
|
+
|
64
|
+
def ==(other)
|
65
|
+
other.instance_of?(self.class) && fuse == other.fuse
|
66
|
+
end
|
67
|
+
|
68
|
+
def open?
|
69
|
+
fuse.state == :open
|
70
|
+
end
|
71
|
+
alias down? open?
|
72
|
+
|
73
|
+
def closed?
|
74
|
+
fuse.state == :closed
|
75
|
+
end
|
76
|
+
alias up? closed?
|
77
|
+
|
78
|
+
def retry_timeout
|
79
|
+
fuse.retry_timeout
|
80
|
+
end
|
81
|
+
|
82
|
+
def failure_count
|
83
|
+
fuse.failure_count
|
84
|
+
end
|
85
|
+
|
86
|
+
def failure_threshold
|
87
|
+
fuse.failure_threshold
|
88
|
+
end
|
89
|
+
|
90
|
+
def timeout
|
91
|
+
fuse.timeout
|
92
|
+
end
|
93
|
+
|
94
|
+
def run(clock = Time.now)
|
95
|
+
if closed? || half_open?(clock)
|
96
|
+
begin
|
97
|
+
result = Timeout.timeout timeout do
|
98
|
+
yield
|
99
|
+
end
|
100
|
+
|
101
|
+
if half_open?(clock)
|
102
|
+
close
|
103
|
+
end
|
104
|
+
|
105
|
+
result
|
106
|
+
rescue => ex
|
107
|
+
fuse.failure_count = fuse.failure_count + 1
|
108
|
+
fuse.retry_threshold = clock + retry_timeout
|
109
|
+
|
110
|
+
open clock
|
111
|
+
|
112
|
+
raise ex
|
113
|
+
end
|
114
|
+
else
|
115
|
+
raise Breaker::CircuitOpenError, "Cannot run code while #{name} is open!"
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
private
|
120
|
+
def tripped?
|
121
|
+
fuse.failure_count != 0
|
122
|
+
end
|
123
|
+
|
124
|
+
def half_open?(clock)
|
125
|
+
tripped? && clock >= fuse.retry_threshold
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
require_relative 'breaker/in_memory_repo'
|
131
|
+
require_relative 'breaker/test_cases'
|
132
|
+
|
133
|
+
Breaker.repo = Breaker::InMemoryRepo.new
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module Breaker
|
2
|
+
class InMemoryRepo
|
3
|
+
Fuse = Struct.new :name, :state, :failure_threshold, :retry_timeout, :timeout, :failure_count, :retry_threshold do
|
4
|
+
def initialize(*args)
|
5
|
+
super
|
6
|
+
|
7
|
+
self.failure_threshold ||= 10
|
8
|
+
self.retry_timeout ||= 60
|
9
|
+
self.timeout ||= 5
|
10
|
+
|
11
|
+
self.state ||= :closed
|
12
|
+
self.failure_count ||= 0
|
13
|
+
end
|
14
|
+
|
15
|
+
def ==(other)
|
16
|
+
other.instance_of?(self.class) && name == other.name
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
attr_reader :store
|
21
|
+
|
22
|
+
def initialize
|
23
|
+
@store = []
|
24
|
+
end
|
25
|
+
|
26
|
+
def upsert(attributes)
|
27
|
+
existing = named attributes.fetch(:name)
|
28
|
+
if existing
|
29
|
+
update existing, attributes
|
30
|
+
else
|
31
|
+
create attributes
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def count
|
36
|
+
store.length
|
37
|
+
end
|
38
|
+
|
39
|
+
def first
|
40
|
+
store.first
|
41
|
+
end
|
42
|
+
|
43
|
+
def named(name)
|
44
|
+
store.find { |fuse| fuse.name == name }
|
45
|
+
end
|
46
|
+
|
47
|
+
def create(attributes)
|
48
|
+
fuse = Fuse.new
|
49
|
+
|
50
|
+
attributes.each_pair do |key, value|
|
51
|
+
fuse.send "#{key}=", value
|
52
|
+
end
|
53
|
+
|
54
|
+
store << fuse
|
55
|
+
|
56
|
+
fuse
|
57
|
+
end
|
58
|
+
|
59
|
+
def update(existing, attributes)
|
60
|
+
existing
|
61
|
+
|
62
|
+
attributes.each_pair do |key, value|
|
63
|
+
existing.send "#{key}=", value
|
64
|
+
end
|
65
|
+
|
66
|
+
existing
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,159 @@
|
|
1
|
+
module Breaker
|
2
|
+
module TestCases
|
3
|
+
DummyError = Class.new RuntimeError
|
4
|
+
|
5
|
+
def repo
|
6
|
+
flunk "Test must define a repo to use"
|
7
|
+
end
|
8
|
+
|
9
|
+
def setup
|
10
|
+
Breaker.repo = repo
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_new_fuses_start_off_clean
|
14
|
+
circuit = Breaker.circuit 'test'
|
15
|
+
|
16
|
+
assert circuit.closed?, "New circuits should be closed"
|
17
|
+
assert_equal 0, circuit.failure_count
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_goes_into_open_state_when_failure_threshold_reached
|
21
|
+
circuit = Breaker.circuit 'test', failure_threshold: 1, retry_timeout: 30
|
22
|
+
|
23
|
+
assert circuit.closed?
|
24
|
+
|
25
|
+
assert_raises DummyError do
|
26
|
+
circuit.run do
|
27
|
+
raise DummyError
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
assert circuit.open?
|
32
|
+
assert_raises Breaker::CircuitOpenError do
|
33
|
+
circuit.run do
|
34
|
+
assert false, "Block should not run in this state"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def test_success_in_half_open_state_moves_circuit_into_closed
|
40
|
+
clock = Time.now
|
41
|
+
circuit = Breaker.circuit 'test', failure_threshold: 2, retry_timeout: 15
|
42
|
+
|
43
|
+
circuit.open clock
|
44
|
+
assert circuit.open?
|
45
|
+
|
46
|
+
assert_raises Breaker::CircuitOpenError do
|
47
|
+
circuit.run clock do
|
48
|
+
# nothing
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
circuit.run clock + circuit.retry_timeout do
|
53
|
+
# do nothing, this works and flips the circuit back closed
|
54
|
+
end
|
55
|
+
|
56
|
+
assert circuit.closed?
|
57
|
+
end
|
58
|
+
|
59
|
+
def test_failures_in_half_open_state_push_retry_timeout_back
|
60
|
+
clock = Time.now
|
61
|
+
circuit = Breaker.circuit 'test', failure_threshold: 2, retry_timeout: 15
|
62
|
+
|
63
|
+
circuit.open clock
|
64
|
+
assert circuit.open?
|
65
|
+
|
66
|
+
assert_raises DummyError do
|
67
|
+
circuit.run clock + circuit.retry_timeout do
|
68
|
+
raise DummyError
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
assert_raises Breaker::CircuitOpenError do
|
73
|
+
circuit.run clock + circuit.retry_timeout do
|
74
|
+
assert false, "Block should not be run while in this state"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
assert_raises DummyError do
|
79
|
+
circuit.run clock + circuit.retry_timeout * 2 do
|
80
|
+
raise DummyError
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def test_counts_timeouts_as_trips
|
86
|
+
circuit = Breaker.circuit 'test', retry_timeout: 15, timeout: 0.01
|
87
|
+
assert circuit.closed?
|
88
|
+
|
89
|
+
assert_raises TimeoutError do
|
90
|
+
circuit.run do
|
91
|
+
sleep circuit.timeout * 2
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def test_circuit_factory_persists_fuses
|
97
|
+
circuit_a = Breaker.circuit 'test'
|
98
|
+
circuit_b = Breaker.circuit 'test'
|
99
|
+
|
100
|
+
assert_equal circuit_a, circuit_b, "Multiple calls to `circuit` should return the same circuit"
|
101
|
+
|
102
|
+
assert_equal 1, Breaker.repo.count
|
103
|
+
fuse = Breaker.repo.first
|
104
|
+
|
105
|
+
assert_equal 'test', fuse.name
|
106
|
+
end
|
107
|
+
|
108
|
+
def test_circuit_factory_creates_new_fuses_with_sensible_defaults
|
109
|
+
circuit = Breaker.circuit 'test'
|
110
|
+
|
111
|
+
assert_equal 1, Breaker.repo.count
|
112
|
+
fuse = Breaker.repo.first
|
113
|
+
|
114
|
+
assert_equal 10, fuse.failure_threshold, "Failure Theshold should have a default"
|
115
|
+
assert_equal 60, fuse.retry_timeout, "Retry timeout should have a default"
|
116
|
+
assert_equal 5, fuse.timeout, "Timeout should have a default"
|
117
|
+
end
|
118
|
+
|
119
|
+
def test_circuit_factory_updates_existing_fuses
|
120
|
+
Breaker.circuit 'test'
|
121
|
+
assert_equal 1, Breaker.repo.count
|
122
|
+
|
123
|
+
Breaker.circuit 'test', failure_threshold: 1,
|
124
|
+
retry_timeout: 2, timeout: 3
|
125
|
+
|
126
|
+
assert_equal 1, Breaker.repo.count
|
127
|
+
fuse = Breaker.repo.first
|
128
|
+
|
129
|
+
assert_equal 1, fuse.failure_threshold
|
130
|
+
assert_equal 2, fuse.retry_timeout
|
131
|
+
assert_equal 3, fuse.timeout
|
132
|
+
end
|
133
|
+
|
134
|
+
def test_circuit_breaker_factory_can_run_code_through_the_circuit
|
135
|
+
assert_raises DummyError do
|
136
|
+
Breaker.circuit 'test' do
|
137
|
+
raise DummyError
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def test_breaker_query_methods
|
143
|
+
circuit = Breaker.circuit 'test'
|
144
|
+
circuit.close
|
145
|
+
|
146
|
+
assert Breaker.closed?('test')
|
147
|
+
assert Breaker.up?('test')
|
148
|
+
refute Breaker.open?('test')
|
149
|
+
refute Breaker.down?('test')
|
150
|
+
|
151
|
+
circuit.open
|
152
|
+
|
153
|
+
assert Breaker.open?('test')
|
154
|
+
assert Breaker.down?('test')
|
155
|
+
refute Breaker.closed?('test')
|
156
|
+
refute Breaker.up?('test')
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require_relative 'test_helper'
|
2
|
+
|
3
|
+
class AcceptanceTest < MiniTest::Unit::TestCase
|
4
|
+
include Breaker::TestCases
|
5
|
+
|
6
|
+
InMemoryFuse = Struct.new :state, :failure_count, :retry_threshold,
|
7
|
+
:failure_threshold, :retry_timeout, :timeout
|
8
|
+
|
9
|
+
attr_reader :fuse, :repo
|
10
|
+
|
11
|
+
def setup
|
12
|
+
@repo = Breaker::InMemoryRepo.new
|
13
|
+
@fuse = InMemoryFuse.new :closed, 0, nil, 3, 15, 10
|
14
|
+
super
|
15
|
+
end
|
16
|
+
end
|
data/test/test_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: breaker
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- ahawkins
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-01-14 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.3'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.3'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
description: 'Circuit breaker pattern for well designed Ruby applications '
|
42
|
+
email:
|
43
|
+
- adam@hawkins.io
|
44
|
+
executables: []
|
45
|
+
extensions: []
|
46
|
+
extra_rdoc_files: []
|
47
|
+
files:
|
48
|
+
- ".gitignore"
|
49
|
+
- Gemfile
|
50
|
+
- LICENSE.txt
|
51
|
+
- README.md
|
52
|
+
- Rakefile
|
53
|
+
- breaker.gemspec
|
54
|
+
- lib/breaker.rb
|
55
|
+
- lib/breaker/in_memory_repo.rb
|
56
|
+
- lib/breaker/test_cases.rb
|
57
|
+
- lib/breaker/version.rb
|
58
|
+
- test/acceptance_test.rb
|
59
|
+
- test/test_helper.rb
|
60
|
+
homepage: https://github.com/ahawkins/breaker
|
61
|
+
licenses:
|
62
|
+
- MIT
|
63
|
+
metadata: {}
|
64
|
+
post_install_message:
|
65
|
+
rdoc_options: []
|
66
|
+
require_paths:
|
67
|
+
- lib
|
68
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
69
|
+
requirements:
|
70
|
+
- - ">="
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
version: '0'
|
73
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
74
|
+
requirements:
|
75
|
+
- - ">="
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
requirements: []
|
79
|
+
rubyforge_project:
|
80
|
+
rubygems_version: 2.2.0
|
81
|
+
signing_key:
|
82
|
+
specification_version: 4
|
83
|
+
summary: ''
|
84
|
+
test_files:
|
85
|
+
- test/acceptance_test.rb
|
86
|
+
- test/test_helper.rb
|