resilient 0.3.1 → 0.4.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +19 -11
- data/examples/basic.rb +1 -2
- data/examples/get_vs_new.rb +40 -0
- data/examples/long_running.rb +1 -2
- data/lib/resilient/circuit_breaker.rb +48 -5
- data/lib/resilient/circuit_breaker/properties.rb +14 -0
- data/lib/resilient/circuit_breaker/registry.rb +46 -0
- data/lib/resilient/instrumenters/memory.rb +5 -1
- data/lib/resilient/key.rb +22 -0
- data/lib/resilient/test/circuit_breaker_registry_interface.rb +42 -0
- data/lib/resilient/version.rb +1 -1
- metadata +8 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d58c0c8c5444d5e583d6667644d2309fd1d76c75
|
4
|
+
data.tar.gz: 92b729571e650001c424eac22b707eabb5f02806
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: de26fd92ca043468781a0cc18544d8904a37da9170c86f347141519e07e36304f005be212618f3dd909e379e3f694b638be480a483107a2ef9627440c70153d8
|
7
|
+
data.tar.gz: 149eba99b8f5d6e3310d0ee4933d4b3ea23cdbee945730a74f4d3189665a6432fda514b1a40a43e7eb51f89c1cc16bc0368642e8e37850ad26724c18be8cd151
|
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# Resilient
|
2
2
|
|
3
|
-
Some tools to aid in resiliency in
|
3
|
+
Some tools to aid in resiliency in Ruby. For now, just a circuit breaker (~~stolen from~~ based on [hystrix](https://github.com/netflix/hystrix)). Soon much more...
|
4
4
|
|
5
5
|
Nothing asynchronous or thread safe yet either, but open to it and would like to see more around it in the future.
|
6
6
|
|
@@ -25,8 +25,12 @@ Or install it yourself as:
|
|
25
25
|
```ruby
|
26
26
|
require "resilient/circuit_breaker"
|
27
27
|
|
28
|
-
# default properties for circuit
|
29
|
-
|
28
|
+
# default properties for circuit, CircuitBreaker.get is used instead of
|
29
|
+
# CircuitBreaker.new as `get` keeps a registry of circuits by key to prevent
|
30
|
+
# creating multiple instances of the same circuit breaker for a key; not using
|
31
|
+
# `get` means you would have multiple instances of the circuit breaker and thus
|
32
|
+
# separate state and metrics; you can read more in examples/for_vs_new.rb
|
33
|
+
circuit_breaker = Resilient::CircuitBreaker.get("example")
|
30
34
|
if circuit_breaker.allow_request?
|
31
35
|
begin
|
32
36
|
# do something expensive
|
@@ -43,7 +47,7 @@ end
|
|
43
47
|
customize properties of circuit:
|
44
48
|
|
45
49
|
```ruby
|
46
|
-
|
50
|
+
circuit_breaker = Resilient::CircuitBreaker.get("example", {
|
47
51
|
# at what percentage of errors should we open the circuit
|
48
52
|
error_threshold_percentage: 50,
|
49
53
|
# do not try request again for 5 seconds
|
@@ -51,37 +55,41 @@ properties = Resilient::CircuitBreaker::Properties.new({
|
|
51
55
|
# do not open circuit until at least 5 requests have happened
|
52
56
|
request_volume_threshold: 5,
|
53
57
|
})
|
54
|
-
circuit_breaker = Resilient::CircuitBreaker.new(properties: properties, key: Resilient::Key.new("example"))
|
55
58
|
# etc etc etc
|
56
59
|
```
|
57
60
|
|
58
61
|
force the circuit to be always open:
|
59
62
|
|
60
63
|
```ruby
|
61
|
-
|
62
|
-
circuit_breaker = Resilient::CircuitBreaker.new(properties: properties, key: Resilient::Key.new("example"))
|
64
|
+
circuit_breaker = Resilient::CircuitBreaker.get("example", force_open: true)
|
63
65
|
# etc etc etc
|
64
66
|
```
|
65
67
|
|
66
68
|
force the circuit to be always closed (great way to test in production with no impact, all instrumentation still runs which means you can measure in production with config and gain confidence while never actually opening a circuit incorrectly):
|
67
69
|
|
68
70
|
```ruby
|
69
|
-
|
70
|
-
circuit_breaker = Resilient::CircuitBreaker.new(properties: properties, key: Resilient::Key.new("example"))
|
71
|
+
circuit_breaker = Resilient::CircuitBreaker.get("example", force_closed: true)
|
71
72
|
# etc etc etc
|
72
73
|
```
|
73
74
|
|
74
75
|
customize rolling window to be 10 buckets of 1 second each (10 seconds in all):
|
75
76
|
|
76
77
|
```ruby
|
77
|
-
|
78
|
+
circuit_breaker = Resilient::CircuitBreaker.get("example", {
|
78
79
|
window_size_in_seconds: 10,
|
79
80
|
bucket_size_in_seconds: 1,
|
80
81
|
})
|
81
|
-
circuit_breaker = Resilient::CircuitBreaker.new(metrics: metrics, key: Resilient::Key.new("example"))
|
82
82
|
# etc etc etc
|
83
83
|
```
|
84
84
|
|
85
|
+
## Tests
|
86
|
+
|
87
|
+
To ensure that you have a clean circuit for each test case, be sure to run the following in the setup for your tests (which resets each registered circuit breaker) either before every test case or at a minimum each test case that uses circuit breakers.
|
88
|
+
|
89
|
+
```ruby
|
90
|
+
Resilient::CircuitBreaker.reset
|
91
|
+
```
|
92
|
+
|
85
93
|
## Development
|
86
94
|
|
87
95
|
```bash
|
data/examples/basic.rb
CHANGED
@@ -8,12 +8,11 @@ $:.unshift(lib_path)
|
|
8
8
|
require "pp"
|
9
9
|
require "resilient/circuit_breaker"
|
10
10
|
|
11
|
-
|
11
|
+
circuit_breaker = Resilient::CircuitBreaker.get("example", {
|
12
12
|
sleep_window_seconds: 1,
|
13
13
|
request_volume_threshold: 10,
|
14
14
|
error_threshold_percentage: 25,
|
15
15
|
})
|
16
|
-
circuit_breaker = Resilient::CircuitBreaker.new(key: Resilient::Key.new("example"), properties: properties)
|
17
16
|
|
18
17
|
# success
|
19
18
|
if circuit_breaker.allow_request?
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# setting up load path
|
2
|
+
require "pathname"
|
3
|
+
root_path = Pathname(__FILE__).dirname.join("..").expand_path
|
4
|
+
lib_path = root_path.join("lib")
|
5
|
+
$:.unshift(lib_path)
|
6
|
+
|
7
|
+
# by default new is private so people don't use it, this makes it possible to
|
8
|
+
# use it as resilient checks for this env var prior to privatizing new
|
9
|
+
ENV["RESILIENT_PUBLICIZE_NEW"] = "1"
|
10
|
+
|
11
|
+
# requiring stuff for this example
|
12
|
+
require "pp"
|
13
|
+
require "resilient/circuit_breaker"
|
14
|
+
|
15
|
+
instance = Resilient::CircuitBreaker.get("example")
|
16
|
+
instance_using_get = Resilient::CircuitBreaker.get("example")
|
17
|
+
instance_using_new = Resilient::CircuitBreaker.new("example")
|
18
|
+
|
19
|
+
puts "instance equals instance_using_get: #{instance.equal?(instance_using_get)}"
|
20
|
+
puts "instance equals instance_using_new: #{instance.equal?(instance_using_new)}"
|
21
|
+
|
22
|
+
instance.properties.request_volume_threshold.times do
|
23
|
+
instance.failure
|
24
|
+
end
|
25
|
+
|
26
|
+
puts "instance allow_request?: #{instance.allow_request?}"
|
27
|
+
puts "instance_using_get allow_request?: #{instance_using_get.allow_request?}"
|
28
|
+
|
29
|
+
# this instance allows the request because it isn't sharing internal state and
|
30
|
+
# metrics due to being a new allocated instance; the for instance does not
|
31
|
+
# suffer this because it looks up instances in a registry rather than always
|
32
|
+
# generating a new instance even if you use the exact same key as it bypasses
|
33
|
+
# the registry
|
34
|
+
puts "instance_using_new allow_request?: #{instance_using_new.allow_request?}"
|
35
|
+
|
36
|
+
# instance equals instance_using_get: true
|
37
|
+
# instance equals instance_using_new: false
|
38
|
+
# instance allow_request?: false
|
39
|
+
# instance_using_get allow_request?: false
|
40
|
+
# instance_using_new allow_request?: true
|
data/examples/long_running.rb
CHANGED
@@ -8,14 +8,13 @@ $:.unshift(lib_path)
|
|
8
8
|
require "pp"
|
9
9
|
require "resilient/circuit_breaker"
|
10
10
|
|
11
|
-
|
11
|
+
circuit_breaker = Resilient::CircuitBreaker.get("example", {
|
12
12
|
sleep_window_seconds: 5,
|
13
13
|
request_volume_threshold: 20,
|
14
14
|
error_threshold_percentage: 10,
|
15
15
|
window_size_in_seconds: 60,
|
16
16
|
bucket_size_in_seconds: 1,
|
17
17
|
})
|
18
|
-
circuit_breaker = Resilient::CircuitBreaker.new(key: Resilient::Key.new("example"), properties: properties)
|
19
18
|
|
20
19
|
iterations = 0
|
21
20
|
loop do
|
@@ -1,24 +1,67 @@
|
|
1
1
|
require "resilient/key"
|
2
2
|
require "resilient/circuit_breaker/metrics"
|
3
3
|
require "resilient/circuit_breaker/properties"
|
4
|
+
require "resilient/circuit_breaker/registry"
|
4
5
|
|
5
6
|
module Resilient
|
6
7
|
class CircuitBreaker
|
8
|
+
# Public: Resets all registered circuit breakers (and thus their state/metrics).
|
9
|
+
# Useful for ensuring a clean environment between tests. If you are using a
|
10
|
+
# registry other than the default, you will need to handle resets on your own.
|
11
|
+
def self.reset
|
12
|
+
Registry.default.reset
|
13
|
+
end
|
14
|
+
|
15
|
+
# Public: Returns an instance of circuit breaker based on key and registry.
|
16
|
+
# Default registry is used if none is provided. If key does not exist, it is
|
17
|
+
# registered. If key does exist, it returns registered instance instead of
|
18
|
+
# allocating a new instance in order to ensure that state/metrics are the
|
19
|
+
# same per key.
|
20
|
+
#
|
21
|
+
# See #initialize for docs on key, properties and metrics.
|
22
|
+
def self.get(key, properties = nil, metrics = nil, registry = nil)
|
23
|
+
key = Key.wrap(key)
|
24
|
+
(registry || Registry.default).fetch(key) {
|
25
|
+
new(key, properties, metrics)
|
26
|
+
}
|
27
|
+
end
|
28
|
+
|
29
|
+
unless ENV.key?("RESILIENT_PUBLICIZE_NEW")
|
30
|
+
class << self
|
31
|
+
private :new
|
32
|
+
private :allocate
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
7
36
|
attr_reader :key
|
8
37
|
attr_reader :open
|
9
38
|
attr_reader :opened_or_last_checked_at_epoch
|
10
39
|
attr_reader :metrics
|
11
40
|
attr_reader :properties
|
12
41
|
|
13
|
-
|
14
|
-
|
42
|
+
# Private: Builds new instance of a CircuitBreaker.
|
43
|
+
#
|
44
|
+
# key - The String or Resilient::Key that determines uniqueness of the
|
45
|
+
# circuit breaker in the registry and for instrumentation.
|
46
|
+
#
|
47
|
+
# properties - The Hash or Resilient::CircuitBreaker::Properties that determine how the
|
48
|
+
# circuit breaker should behave. Optional. Defaults to new
|
49
|
+
# Resilient::CircuitBreaker::Properties instance.
|
50
|
+
#
|
51
|
+
# metrics - The object that stores successes and failures. Optional.
|
52
|
+
# Defaults to new Resilient::CircuitBreaker::Metrics instance
|
53
|
+
# based on window size and bucket size properties.
|
54
|
+
#
|
55
|
+
# Returns CircuitBreaker instance.
|
56
|
+
def initialize(key, properties = nil, metrics = nil)
|
15
57
|
raise ArgumentError, "key argument is required" if key.nil?
|
16
|
-
|
17
|
-
@
|
58
|
+
|
59
|
+
@key = Key.wrap(key)
|
60
|
+
@open = false
|
18
61
|
@opened_or_last_checked_at_epoch = 0
|
19
62
|
|
20
63
|
@properties = if properties
|
21
|
-
properties
|
64
|
+
Properties.wrap(properties)
|
22
65
|
else
|
23
66
|
Properties.new
|
24
67
|
end
|
@@ -3,6 +3,20 @@ require "resilient/instrumenters/noop"
|
|
3
3
|
module Resilient
|
4
4
|
class CircuitBreaker
|
5
5
|
class Properties
|
6
|
+
|
7
|
+
# Internal: Takes a string name or instance of a Key and always returns a
|
8
|
+
# Key instance.
|
9
|
+
def self.wrap(hash_or_instance)
|
10
|
+
case hash_or_instance
|
11
|
+
when self
|
12
|
+
hash_or_instance
|
13
|
+
when Hash
|
14
|
+
new(hash_or_instance)
|
15
|
+
else
|
16
|
+
raise TypeError, "properties must be Hash or Resilient::Properties instance"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
6
20
|
# allows forcing the circuit open (stopping all requests)
|
7
21
|
attr_reader :force_open
|
8
22
|
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Resilient
|
2
|
+
class CircuitBreaker
|
3
|
+
class Registry
|
4
|
+
# Internal: Default registry to use for circuit breakers.
|
5
|
+
def self.default
|
6
|
+
@default
|
7
|
+
end
|
8
|
+
|
9
|
+
# Internal: Allows overriding default registry for circuit breakers.
|
10
|
+
def self.default=(value)
|
11
|
+
@default = value
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(source = nil)
|
15
|
+
@source = source || {}
|
16
|
+
end
|
17
|
+
|
18
|
+
# Setup default to new instance.
|
19
|
+
@default = new
|
20
|
+
|
21
|
+
# Internal: To be used by CircuitBreaker to either get an instance for a
|
22
|
+
# key or set a new instance for a key.
|
23
|
+
#
|
24
|
+
# Raises KeyError if key not found and no block provided.
|
25
|
+
def fetch(key, &block)
|
26
|
+
if value = @source[key]
|
27
|
+
value
|
28
|
+
else
|
29
|
+
if block_given?
|
30
|
+
@source[key] = yield
|
31
|
+
else
|
32
|
+
@source.fetch(key)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Internal: To be used by CircuitBreaker to reset the stored circuit
|
38
|
+
# breakers, which should only really be used for cleaning up in
|
39
|
+
# test environment.
|
40
|
+
def reset
|
41
|
+
@source.each_value(&:reset)
|
42
|
+
nil
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -8,7 +8,7 @@ module Resilient
|
|
8
8
|
attr_reader :events
|
9
9
|
|
10
10
|
def initialize
|
11
|
-
|
11
|
+
reset
|
12
12
|
end
|
13
13
|
|
14
14
|
def instrument(name, payload = {})
|
@@ -25,6 +25,10 @@ module Resilient
|
|
25
25
|
@events << Event.new(name, payload, result)
|
26
26
|
result
|
27
27
|
end
|
28
|
+
|
29
|
+
def reset
|
30
|
+
@events = []
|
31
|
+
end
|
28
32
|
end
|
29
33
|
end
|
30
34
|
end
|
data/lib/resilient/key.rb
CHANGED
@@ -1,9 +1,31 @@
|
|
1
1
|
module Resilient
|
2
2
|
class Key
|
3
|
+
|
4
|
+
# Internal: Takes a string name or instance of a Key and always returns a
|
5
|
+
# Key instance.
|
6
|
+
def self.wrap(string_or_instance)
|
7
|
+
case string_or_instance
|
8
|
+
when self, NilClass
|
9
|
+
string_or_instance
|
10
|
+
else
|
11
|
+
new(string_or_instance)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
3
15
|
attr_reader :name
|
4
16
|
|
5
17
|
def initialize(name)
|
18
|
+
raise TypeError, "name must be a String" unless name.is_a?(String)
|
6
19
|
@name = name
|
7
20
|
end
|
21
|
+
|
22
|
+
def ==(other)
|
23
|
+
self.class == other.class && name == other.name
|
24
|
+
end
|
25
|
+
alias_method :eql?, :==
|
26
|
+
|
27
|
+
def hash
|
28
|
+
@name.hash
|
29
|
+
end
|
8
30
|
end
|
9
31
|
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Resilient
|
2
|
+
class Test
|
3
|
+
module CircuitBreakerRegistryInterface
|
4
|
+
def test_responds_to_fetch
|
5
|
+
assert_respond_to @object, :fetch
|
6
|
+
end
|
7
|
+
|
8
|
+
def test_responds_to_reset
|
9
|
+
assert_respond_to @object, :reset
|
10
|
+
end
|
11
|
+
|
12
|
+
def test_fetch
|
13
|
+
key = "foo"
|
14
|
+
value = "bar".freeze
|
15
|
+
|
16
|
+
assert_raises(KeyError) { @object.fetch(key) }
|
17
|
+
assert_equal value, @object.fetch(key) { value }
|
18
|
+
assert_equal value, @object.fetch(key)
|
19
|
+
assert @object.fetch(key).equal?(value)
|
20
|
+
end
|
21
|
+
|
22
|
+
def test_reset
|
23
|
+
bar_value = Minitest::Mock.new
|
24
|
+
wick_value = Minitest::Mock.new
|
25
|
+
bar_value.expect :reset, nil, []
|
26
|
+
wick_value.expect :reset, nil, []
|
27
|
+
|
28
|
+
@object.fetch("foo") { bar_value }
|
29
|
+
@object.fetch("baz") { wick_value }
|
30
|
+
|
31
|
+
assert_nil @object.reset
|
32
|
+
|
33
|
+
bar_value.verify
|
34
|
+
wick_value.verify
|
35
|
+
end
|
36
|
+
|
37
|
+
def test_reset_empty_registry
|
38
|
+
assert_nil @object.reset
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
data/lib/resilient/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: resilient
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0.beta1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- John Nunemaker
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-
|
11
|
+
date: 2016-04-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -66,6 +66,7 @@ files:
|
|
66
66
|
- LICENSE.txt
|
67
67
|
- README.md
|
68
68
|
- examples/basic.rb
|
69
|
+
- examples/get_vs_new.rb
|
69
70
|
- examples/long_running.rb
|
70
71
|
- lib/resilient.rb
|
71
72
|
- lib/resilient/circuit_breaker.rb
|
@@ -76,10 +77,12 @@ files:
|
|
76
77
|
- lib/resilient/circuit_breaker/metrics/storage/memory.rb
|
77
78
|
- lib/resilient/circuit_breaker/metrics/window_size.rb
|
78
79
|
- lib/resilient/circuit_breaker/properties.rb
|
80
|
+
- lib/resilient/circuit_breaker/registry.rb
|
79
81
|
- lib/resilient/instrumenters/memory.rb
|
80
82
|
- lib/resilient/instrumenters/noop.rb
|
81
83
|
- lib/resilient/key.rb
|
82
84
|
- lib/resilient/test/circuit_breaker_interface.rb
|
85
|
+
- lib/resilient/test/circuit_breaker_registry_interface.rb
|
83
86
|
- lib/resilient/test/metrics_interface.rb
|
84
87
|
- lib/resilient/test/metrics_storage_interface.rb
|
85
88
|
- lib/resilient/test/properties_interface.rb
|
@@ -105,12 +108,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
105
108
|
version: '0'
|
106
109
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
107
110
|
requirements:
|
108
|
-
- - "
|
111
|
+
- - ">"
|
109
112
|
- !ruby/object:Gem::Version
|
110
|
-
version:
|
113
|
+
version: 1.3.1
|
111
114
|
requirements: []
|
112
115
|
rubyforge_project:
|
113
|
-
rubygems_version: 2.
|
116
|
+
rubygems_version: 2.4.5.1
|
114
117
|
signing_key:
|
115
118
|
specification_version: 4
|
116
119
|
summary: toolkit for resilient ruby apps
|