resilient 0.1.0 → 0.2.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/.gitignore +1 -0
- data/Gemfile +0 -3
- data/Guardfile +0 -18
- data/README.md +21 -15
- data/examples/basic.rb +8 -8
- data/examples/long_running.rb +5 -5
- data/lib/resilient.rb +2 -0
- data/lib/resilient/circuit_breaker.rb +42 -25
- data/lib/resilient/circuit_breaker/metrics.rb +12 -65
- data/lib/resilient/circuit_breaker/metrics/bucket.rb +23 -0
- data/lib/resilient/circuit_breaker/metrics/bucket_range.rb +25 -0
- data/lib/resilient/circuit_breaker/metrics/bucket_size.rb +27 -0
- data/lib/resilient/circuit_breaker/metrics/storage/memory.rb +1 -15
- data/lib/resilient/circuit_breaker/metrics/window_size.rb +15 -0
- data/lib/resilient/circuit_breaker/properties.rb +56 -0
- data/lib/resilient/key.rb +9 -0
- data/lib/resilient/test/circuit_breaker_interface.rb +12 -9
- data/lib/resilient/test/metrics_interface.rb +8 -8
- data/lib/resilient/test/metrics_storage_interface.rb +8 -59
- data/lib/resilient/test/{config_interface.rb → properties_interface.rb} +1 -5
- data/lib/resilient/version.rb +1 -1
- data/resilient.gemspec +3 -3
- data/script/bootstrap +14 -0
- data/script/watch +14 -0
- metadata +25 -21
- data/Rakefile +0 -10
- data/lib/resilient/circuit_breaker/config.rb +0 -27
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5cebeacad55c7a80623379752b452f9a20dd0009
|
4
|
+
data.tar.gz: 9d56f7dc8b598304b8f2aea2e6c46a743f0820f2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 63d8cb0a2f6d784573a22df49b407530ebbbe8e8cc534c678ff92d89f749e62ab9c906eb17ad490aad92ede7bf5f8bbeffa0a8e265f801b29a20131804d28647
|
7
|
+
data.tar.gz: 02023b4a9de5bb484fb732c28e11538aa33f0a6ee01e84bff4d1f5c85ae483886e6d1171af99a86eeebbaa5256909f0cce0a4213e4f6d24144e330566b0c6374
|
data/.gitignore
CHANGED
data/Gemfile
CHANGED
data/Guardfile
CHANGED
@@ -1,23 +1,5 @@
|
|
1
|
-
# A sample Guardfile
|
2
|
-
# More info at https://github.com/guard/guard#readme
|
3
|
-
|
4
|
-
## Uncomment and set this to only include directories you want to watch
|
5
|
-
# directories %w(app lib config test spec features) \
|
6
|
-
# .select{|d| Dir.exists?(d) ? d : UI.warning("Directory #{d} does not exist")}
|
7
|
-
|
8
|
-
## Note: if you are using the `directories` clause above and you are not
|
9
|
-
## watching the project directory ('.'), then you will want to move
|
10
|
-
## the Guardfile to a watched dir and symlink it back, e.g.
|
11
|
-
#
|
12
|
-
# $ mkdir config
|
13
|
-
# $ mv Guardfile config/
|
14
|
-
# $ ln -s config/Guardfile .
|
15
|
-
#
|
16
|
-
# and, you'll have to watch "config/Guardfile" instead of "Guardfile"
|
17
|
-
|
18
1
|
guard :minitest do
|
19
2
|
watch(%r{^test/(.*)\/?(.*)_test\.rb$}) { "test" }
|
20
3
|
watch(%r{^lib/(.*/)?([^/]+)\.rb$}) { "test" }
|
21
4
|
watch(%r{^test/test_helper\.rb$}) { "test" }
|
22
|
-
watch(%r{test/support/.*}) { "test" }
|
23
5
|
end
|
data/README.md
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
# Resilient
|
2
2
|
|
3
|
-
Some tools
|
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
|
-
Nothing asynchronous or thread safe yet either.
|
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
|
|
7
7
|
## Installation
|
8
8
|
|
@@ -25,14 +25,14 @@ Or install it yourself as:
|
|
25
25
|
```ruby
|
26
26
|
require "resilient/circuit_breaker"
|
27
27
|
|
28
|
-
# default
|
29
|
-
circuit_breaker = Resilient::CircuitBreaker.new
|
28
|
+
# default properties for circuit
|
29
|
+
circuit_breaker = Resilient::CircuitBreaker.new(key: Resilient::Key.new("example"))
|
30
30
|
if circuit_breaker.allow_request?
|
31
31
|
begin
|
32
32
|
# do something expensive
|
33
|
-
circuit_breaker.
|
33
|
+
circuit_breaker.success
|
34
34
|
rescue => boom
|
35
|
-
circuit_breaker.
|
35
|
+
circuit_breaker.failure
|
36
36
|
# do fallback
|
37
37
|
end
|
38
38
|
else
|
@@ -40,10 +40,10 @@ else
|
|
40
40
|
end
|
41
41
|
```
|
42
42
|
|
43
|
-
customize
|
43
|
+
customize properties of circuit:
|
44
44
|
|
45
45
|
```ruby
|
46
|
-
|
46
|
+
properties = Resilient::CircuitBreaker::Properties.new({
|
47
47
|
# at what percentage of errors should we open the circuit
|
48
48
|
error_threshold_percentage: 50,
|
49
49
|
# do not try request again for 5 seconds
|
@@ -51,23 +51,23 @@ config = Resilient::CircuitBreaker::Config.new({
|
|
51
51
|
# do not open circuit until at least 5 requests have happened
|
52
52
|
request_volume_threshold: 5,
|
53
53
|
})
|
54
|
-
circuit_breaker = Resilient::CircuitBreaker.new(
|
54
|
+
circuit_breaker = Resilient::CircuitBreaker.new(properties: properties, key: Resilient::Key.new("example"))
|
55
55
|
# etc etc etc
|
56
56
|
```
|
57
57
|
|
58
58
|
force the circuit to be always open:
|
59
59
|
|
60
60
|
```ruby
|
61
|
-
|
62
|
-
circuit_breaker = Resilient::CircuitBreaker.new(
|
61
|
+
properties = Resilient::CircuitBreaker::Properties.new(force_open: true)
|
62
|
+
circuit_breaker = Resilient::CircuitBreaker.new(properties: properties, key: Resilient::Key.new("example"))
|
63
63
|
# etc etc etc
|
64
64
|
```
|
65
65
|
|
66
66
|
force the circuit to be always closed:
|
67
67
|
|
68
68
|
```ruby
|
69
|
-
|
70
|
-
circuit_breaker = Resilient::CircuitBreaker.new(
|
69
|
+
properties = Resilient::CircuitBreaker::Properties.new(force_closed: true)
|
70
|
+
circuit_breaker = Resilient::CircuitBreaker.new(properties: properties, key: Resilient::Key.new("example"))
|
71
71
|
# etc etc etc
|
72
72
|
```
|
73
73
|
|
@@ -75,10 +75,10 @@ customize rolling window to be 10 buckets of 1 second each (10 seconds in all):
|
|
75
75
|
|
76
76
|
```ruby
|
77
77
|
metrics = Resilient::CircuitBreaker::Metrics.new({
|
78
|
-
|
78
|
+
window_size_in_seconds: 10,
|
79
79
|
bucket_size_in_seconds: 1,
|
80
80
|
})
|
81
|
-
circuit_breaker = Resilient::CircuitBreaker.new(metrics: metrics)
|
81
|
+
circuit_breaker = Resilient::CircuitBreaker.new(metrics: metrics, key: Resilient::Key.new("example"))
|
82
82
|
# etc etc etc
|
83
83
|
```
|
84
84
|
|
@@ -105,3 +105,9 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/jnunem
|
|
105
105
|
## License
|
106
106
|
|
107
107
|
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
108
|
+
|
109
|
+
## Release (for maintainers)
|
110
|
+
|
111
|
+
* increment version based on semver
|
112
|
+
* git commit version change
|
113
|
+
* script/release (releases to rubygems and git tags)
|
data/examples/basic.rb
CHANGED
@@ -8,21 +8,21 @@ $:.unshift(lib_path)
|
|
8
8
|
require "pp"
|
9
9
|
require "resilient/circuit_breaker"
|
10
10
|
|
11
|
-
|
11
|
+
properties = Resilient::CircuitBreaker::Properties.new({
|
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(
|
16
|
+
circuit_breaker = Resilient::CircuitBreaker.new(key: Resilient::Key.new("example"), properties: properties)
|
17
17
|
|
18
18
|
# success
|
19
19
|
if circuit_breaker.allow_request?
|
20
20
|
begin
|
21
21
|
puts "do expensive thing"
|
22
|
-
circuit_breaker.
|
22
|
+
circuit_breaker.success
|
23
23
|
rescue => boom
|
24
24
|
# won't get here in this example
|
25
|
-
circuit_breaker.
|
25
|
+
circuit_breaker.failure
|
26
26
|
end
|
27
27
|
else
|
28
28
|
raise "will not get here"
|
@@ -33,7 +33,7 @@ if circuit_breaker.allow_request?
|
|
33
33
|
begin
|
34
34
|
raise
|
35
35
|
rescue => boom
|
36
|
-
circuit_breaker.
|
36
|
+
circuit_breaker.failure
|
37
37
|
puts "failed slow, do fallback"
|
38
38
|
end
|
39
39
|
else
|
@@ -42,8 +42,8 @@ end
|
|
42
42
|
|
43
43
|
# trip circuit, imagine this being same as above but in real life...
|
44
44
|
# also, we have to fail at least the request volume threshold number of times
|
45
|
-
circuit_breaker.
|
46
|
-
circuit_breaker.
|
45
|
+
circuit_breaker.properties.request_volume_threshold.times do
|
46
|
+
circuit_breaker.failure
|
47
47
|
end
|
48
48
|
|
49
49
|
# fail fast
|
@@ -72,7 +72,7 @@ else
|
|
72
72
|
end
|
73
73
|
|
74
74
|
puts "marking single request as success"
|
75
|
-
circuit_breaker.
|
75
|
+
circuit_breaker.success
|
76
76
|
|
77
77
|
if circuit_breaker.allow_request?
|
78
78
|
puts "circuit reset and back closed now, allowing requests"
|
data/examples/long_running.rb
CHANGED
@@ -8,14 +8,14 @@ $:.unshift(lib_path)
|
|
8
8
|
require "pp"
|
9
9
|
require "resilient/circuit_breaker"
|
10
10
|
|
11
|
-
|
11
|
+
properties = Resilient::CircuitBreaker::Properties.new({
|
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(
|
18
|
+
circuit_breaker = Resilient::CircuitBreaker.new(key: Resilient::Key.new("example"), properties: properties)
|
19
19
|
|
20
20
|
iterations = 0
|
21
21
|
loop do
|
@@ -24,10 +24,10 @@ loop do
|
|
24
24
|
puts "request allowed"
|
25
25
|
raise if rand(100) < 10
|
26
26
|
puts "request succeeded"
|
27
|
-
circuit_breaker.
|
27
|
+
circuit_breaker.success
|
28
28
|
rescue => boom
|
29
29
|
puts "request failed"
|
30
|
-
circuit_breaker.
|
30
|
+
circuit_breaker.failure
|
31
31
|
end
|
32
32
|
else
|
33
33
|
puts "request denied"
|
data/lib/resilient.rb
CHANGED
@@ -1,40 +1,52 @@
|
|
1
|
+
require "resilient/key"
|
1
2
|
require "resilient/circuit_breaker/metrics"
|
2
|
-
require "resilient/circuit_breaker/
|
3
|
+
require "resilient/circuit_breaker/properties"
|
3
4
|
|
4
5
|
module Resilient
|
5
6
|
class CircuitBreaker
|
6
|
-
attr_reader :
|
7
|
-
attr_reader :config
|
7
|
+
attr_reader :key
|
8
8
|
attr_reader :open
|
9
9
|
attr_reader :opened_or_last_checked_at_epoch
|
10
|
+
attr_reader :metrics
|
11
|
+
attr_reader :properties
|
10
12
|
|
11
|
-
def initialize(open: false,
|
13
|
+
def initialize(key: nil, open: false, properties: nil, metrics: nil)
|
14
|
+
# ruby 2.0 does not support required keyword arguments, this gets around that
|
15
|
+
raise ArgumentError, "key argument is required" if key.nil?
|
16
|
+
@key = key
|
12
17
|
@open = open
|
13
18
|
@opened_or_last_checked_at_epoch = 0
|
14
|
-
|
19
|
+
|
20
|
+
@properties = if properties
|
21
|
+
properties
|
22
|
+
else
|
23
|
+
Properties.new
|
24
|
+
end
|
25
|
+
|
15
26
|
@metrics = if metrics
|
16
27
|
metrics
|
17
28
|
else
|
18
29
|
Metrics.new({
|
19
|
-
|
20
|
-
bucket_size_in_seconds:
|
30
|
+
window_size_in_seconds: @properties.window_size_in_seconds,
|
31
|
+
bucket_size_in_seconds: @properties.bucket_size_in_seconds,
|
21
32
|
})
|
22
33
|
end
|
23
34
|
end
|
24
35
|
|
25
36
|
def allow_request?
|
26
37
|
default_payload = {
|
38
|
+
key: @key,
|
27
39
|
force_open: false,
|
28
40
|
force_closed: false,
|
29
41
|
}
|
30
42
|
|
31
43
|
instrument("resilient.circuit_breaker.allow_request", default_payload) { |payload|
|
32
|
-
result = if payload[:force_open] = @
|
44
|
+
result = if payload[:force_open] = @properties.force_open
|
33
45
|
false
|
34
46
|
else
|
35
|
-
if payload[:force_closed] = @
|
47
|
+
if payload[:force_closed] = @properties.force_closed
|
36
48
|
# we still want to simulate normal behavior/metrics like open, allow
|
37
|
-
# single request, etc. so it is possible to test
|
49
|
+
# single request, etc. so it is possible to test properties in
|
38
50
|
# production without impact
|
39
51
|
if payload[:open] = open?
|
40
52
|
payload[:allow_single_request] = allow_single_request?
|
@@ -54,31 +66,40 @@ module Resilient
|
|
54
66
|
}
|
55
67
|
end
|
56
68
|
|
57
|
-
def
|
69
|
+
def success
|
58
70
|
default_payload = {
|
71
|
+
key: @key,
|
59
72
|
closed_the_circuit: false,
|
60
73
|
}
|
61
74
|
|
62
|
-
instrument("resilient.circuit_breaker.
|
75
|
+
instrument("resilient.circuit_breaker.success", default_payload) { |payload|
|
63
76
|
if @open
|
64
77
|
payload[:closed_the_circuit] = true
|
65
78
|
close_circuit
|
66
79
|
else
|
67
|
-
@metrics.
|
80
|
+
@metrics.success
|
68
81
|
end
|
69
82
|
nil
|
70
83
|
}
|
71
84
|
end
|
72
85
|
|
73
|
-
def
|
74
|
-
|
75
|
-
@
|
86
|
+
def failure
|
87
|
+
default_payload = {
|
88
|
+
key: @key,
|
89
|
+
}
|
90
|
+
|
91
|
+
instrument("resilient.circuit_breaker.failure", default_payload) { |payload|
|
92
|
+
@metrics.failure
|
76
93
|
nil
|
77
94
|
}
|
78
95
|
end
|
79
96
|
|
80
97
|
def reset
|
81
|
-
|
98
|
+
default_payload = {
|
99
|
+
key: @key,
|
100
|
+
}
|
101
|
+
|
102
|
+
instrument("resilient.circuit_breaker.reset", default_payload) { |payload|
|
82
103
|
@open = false
|
83
104
|
@opened_or_last_checked_at_epoch = 0
|
84
105
|
@metrics.reset
|
@@ -100,11 +121,11 @@ module Resilient
|
|
100
121
|
end
|
101
122
|
|
102
123
|
def under_request_volume_threshold?
|
103
|
-
@metrics.requests < @
|
124
|
+
@metrics.requests < @properties.request_volume_threshold
|
104
125
|
end
|
105
126
|
|
106
127
|
def under_error_threshold_percentage?
|
107
|
-
@metrics.error_percentage < @
|
128
|
+
@metrics.error_percentage < @properties.error_threshold_percentage
|
108
129
|
end
|
109
130
|
|
110
131
|
def open?
|
@@ -116,14 +137,10 @@ module Resilient
|
|
116
137
|
true
|
117
138
|
end
|
118
139
|
|
119
|
-
def closed?
|
120
|
-
!open?
|
121
|
-
end
|
122
|
-
|
123
140
|
def allow_single_request?
|
124
141
|
now = Time.now.to_i
|
125
142
|
|
126
|
-
if @open && now > (@opened_or_last_checked_at_epoch + @
|
143
|
+
if @open && now > (@opened_or_last_checked_at_epoch + @properties.sleep_window_seconds)
|
127
144
|
@opened_or_last_checked_at_epoch = now
|
128
145
|
true
|
129
146
|
else
|
@@ -132,7 +149,7 @@ module Resilient
|
|
132
149
|
end
|
133
150
|
|
134
151
|
def instrument(name, payload = {}, &block)
|
135
|
-
|
152
|
+
properties.instrumenter.instrument(name, payload, &block)
|
136
153
|
end
|
137
154
|
end
|
138
155
|
end
|
@@ -1,9 +1,12 @@
|
|
1
1
|
require "resilient/circuit_breaker/metrics/storage/memory"
|
2
|
+
require "resilient/circuit_breaker/metrics/bucket_range"
|
3
|
+
require "resilient/circuit_breaker/metrics/bucket_size"
|
4
|
+
require "resilient/circuit_breaker/metrics/window_size"
|
2
5
|
|
3
6
|
module Resilient
|
4
7
|
class CircuitBreaker
|
5
8
|
class Metrics
|
6
|
-
attr_reader :
|
9
|
+
attr_reader :window_size_in_seconds
|
7
10
|
attr_reader :bucket_size_in_seconds
|
8
11
|
attr_reader :buckets
|
9
12
|
attr_reader :storage
|
@@ -18,78 +21,22 @@ module Resilient
|
|
18
21
|
|
19
22
|
StorageKeys = (StorageSuccessKeys + StorageFailureKeys).freeze
|
20
23
|
|
21
|
-
class Bucket
|
22
|
-
attr_reader :timestamp_start
|
23
|
-
attr_reader :timestamp_end
|
24
|
-
|
25
|
-
def initialize(timestamp_start, timestamp_end)
|
26
|
-
@timestamp_start = timestamp_start
|
27
|
-
@timestamp_end = timestamp_end
|
28
|
-
end
|
29
|
-
|
30
|
-
def prune_before(number_of_buckets, bucket_size)
|
31
|
-
@timestamp_end - (number_of_buckets * bucket_size.seconds)
|
32
|
-
end
|
33
|
-
|
34
|
-
def include?(timestamp)
|
35
|
-
timestamp >= @timestamp_start && timestamp <= @timestamp_end
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
class BucketRange
|
40
|
-
def self.generate(timestamp, number_of_buckets, bucket_size)
|
41
|
-
end_bucket = bucket_size.bucket(timestamp)
|
42
|
-
start_bucket = bucket_size.bucket(end_bucket.prune_before(number_of_buckets, bucket_size))
|
43
|
-
bucket_range = new(start_bucket, end_bucket)
|
44
|
-
end
|
45
|
-
|
46
|
-
attr_reader :start_bucket
|
47
|
-
attr_reader :end_bucket
|
48
|
-
|
49
|
-
def initialize(start_bucket, end_bucket)
|
50
|
-
@start_bucket = start_bucket
|
51
|
-
@end_bucket = end_bucket
|
52
|
-
end
|
53
|
-
|
54
|
-
def prune?(bucket)
|
55
|
-
bucket.timestamp_end <= @start_bucket.timestamp_end
|
56
|
-
end
|
57
|
-
end
|
58
|
-
|
59
|
-
class BucketSize
|
60
|
-
attr_reader :seconds
|
61
|
-
|
62
|
-
def initialize(seconds)
|
63
|
-
@seconds = seconds
|
64
|
-
end
|
65
|
-
|
66
|
-
def aligned_start(timestamp = Time.now.to_i)
|
67
|
-
timestamp / @seconds * @seconds
|
68
|
-
end
|
69
|
-
|
70
|
-
def aligned_end(timestamp = Time.now.to_i)
|
71
|
-
aligned_start(timestamp) + @seconds - 1
|
72
|
-
end
|
73
|
-
|
74
|
-
def bucket(timestamp = Time.now.to_i)
|
75
|
-
Bucket.new aligned_start(timestamp), aligned_end(timestamp)
|
76
|
-
end
|
77
|
-
end
|
78
|
-
|
79
24
|
def initialize(options = {})
|
80
|
-
@
|
81
|
-
@
|
25
|
+
@window_size_in_seconds = options.fetch(:window_size_in_seconds, 60)
|
26
|
+
@bucket_size_in_seconds = options.fetch(:bucket_size_in_seconds, 10)
|
27
|
+
@window_size = WindowSize.new(@window_size_in_seconds)
|
28
|
+
@bucket_size = BucketSize.new(@bucket_size_in_seconds)
|
82
29
|
@storage = options.fetch(:storage) { Storage::Memory.new }
|
83
30
|
@buckets = []
|
84
31
|
end
|
85
32
|
|
86
|
-
def
|
33
|
+
def success
|
87
34
|
@storage.increment(current_bucket, StorageSuccessKeys)
|
88
35
|
prune_buckets
|
89
36
|
nil
|
90
37
|
end
|
91
38
|
|
92
|
-
def
|
39
|
+
def failure
|
93
40
|
@storage.increment(current_bucket, StorageFailureKeys)
|
94
41
|
prune_buckets
|
95
42
|
nil
|
@@ -128,7 +75,7 @@ module Resilient
|
|
128
75
|
end
|
129
76
|
|
130
77
|
def reset
|
131
|
-
@storage.
|
78
|
+
@storage.prune(@buckets, StorageKeys)
|
132
79
|
nil
|
133
80
|
end
|
134
81
|
|
@@ -146,7 +93,7 @@ module Resilient
|
|
146
93
|
|
147
94
|
def prune_buckets(timestamp = Time.now.to_i)
|
148
95
|
pruned_buckets = []
|
149
|
-
bucket_range = BucketRange.generate(timestamp, @
|
96
|
+
bucket_range = BucketRange.generate(timestamp, @window_size, @bucket_size)
|
150
97
|
|
151
98
|
@buckets.delete_if { |bucket|
|
152
99
|
if bucket_range.prune?(bucket)
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Resilient
|
2
|
+
class CircuitBreaker
|
3
|
+
class Metrics
|
4
|
+
class Bucket
|
5
|
+
attr_reader :timestamp_start
|
6
|
+
attr_reader :timestamp_end
|
7
|
+
|
8
|
+
def initialize(timestamp_start, timestamp_end)
|
9
|
+
@timestamp_start = timestamp_start
|
10
|
+
@timestamp_end = timestamp_end
|
11
|
+
end
|
12
|
+
|
13
|
+
def prune_before(window_size)
|
14
|
+
@timestamp_end - window_size.seconds
|
15
|
+
end
|
16
|
+
|
17
|
+
def include?(timestamp)
|
18
|
+
timestamp >= @timestamp_start && timestamp <= @timestamp_end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Resilient
|
2
|
+
class CircuitBreaker
|
3
|
+
class Metrics
|
4
|
+
class BucketRange
|
5
|
+
def self.generate(timestamp, window_size, bucket_size)
|
6
|
+
end_bucket = bucket_size.bucket(timestamp)
|
7
|
+
start_bucket = bucket_size.bucket(end_bucket.prune_before(window_size))
|
8
|
+
bucket_range = new(start_bucket, end_bucket)
|
9
|
+
end
|
10
|
+
|
11
|
+
attr_reader :start_bucket
|
12
|
+
attr_reader :end_bucket
|
13
|
+
|
14
|
+
def initialize(start_bucket, end_bucket)
|
15
|
+
@start_bucket = start_bucket
|
16
|
+
@end_bucket = end_bucket
|
17
|
+
end
|
18
|
+
|
19
|
+
def prune?(bucket)
|
20
|
+
bucket.timestamp_end <= @start_bucket.timestamp_end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require "resilient/circuit_breaker/metrics/bucket"
|
2
|
+
|
3
|
+
module Resilient
|
4
|
+
class CircuitBreaker
|
5
|
+
class Metrics
|
6
|
+
class BucketSize
|
7
|
+
attr_reader :seconds
|
8
|
+
|
9
|
+
def initialize(seconds)
|
10
|
+
@seconds = seconds
|
11
|
+
end
|
12
|
+
|
13
|
+
def aligned_start(timestamp = Time.now.to_i)
|
14
|
+
timestamp / @seconds * @seconds
|
15
|
+
end
|
16
|
+
|
17
|
+
def aligned_end(timestamp = Time.now.to_i)
|
18
|
+
aligned_start(timestamp) + @seconds - 1
|
19
|
+
end
|
20
|
+
|
21
|
+
def bucket(timestamp = Time.now.to_i)
|
22
|
+
Bucket.new aligned_start(timestamp), aligned_end(timestamp)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -17,16 +17,6 @@ module Resilient
|
|
17
17
|
end
|
18
18
|
end
|
19
19
|
|
20
|
-
def get(buckets, keys)
|
21
|
-
result = Hash.new { |h, k| h[k] = Hash.new(0) }
|
22
|
-
Array(buckets).each do |bucket|
|
23
|
-
Array(keys).each do |key|
|
24
|
-
result[bucket][key] = @source[bucket][key]
|
25
|
-
end
|
26
|
-
end
|
27
|
-
result
|
28
|
-
end
|
29
|
-
|
30
20
|
def sum(buckets, keys)
|
31
21
|
response = Hash.new(0)
|
32
22
|
Array(buckets).each do |bucket|
|
@@ -37,15 +27,11 @@ module Resilient
|
|
37
27
|
response
|
38
28
|
end
|
39
29
|
|
40
|
-
def
|
30
|
+
def prune(buckets, keys)
|
41
31
|
Array(buckets).each do |bucket|
|
42
32
|
@source.delete(bucket)
|
43
33
|
end
|
44
34
|
end
|
45
|
-
|
46
|
-
def prune(buckets, keys)
|
47
|
-
reset(buckets, keys)
|
48
|
-
end
|
49
35
|
end
|
50
36
|
end
|
51
37
|
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require "resilient/instrumenters/noop"
|
2
|
+
|
3
|
+
module Resilient
|
4
|
+
class CircuitBreaker
|
5
|
+
class Properties
|
6
|
+
# allows forcing the circuit open (stopping all requests)
|
7
|
+
attr_reader :force_open
|
8
|
+
|
9
|
+
# allows ignoring errors and therefore never trip "open"
|
10
|
+
# (ie. allow all traffic through); normal instrumentation will still
|
11
|
+
# happen, thus allowing you to "test" configuration live without impact
|
12
|
+
attr_reader :force_closed
|
13
|
+
|
14
|
+
# what to use to instrument all events that happen
|
15
|
+
# (ie: ActiveSupport::Notifications)
|
16
|
+
attr_reader :instrumenter
|
17
|
+
|
18
|
+
# seconds after tripping circuit before allowing retry
|
19
|
+
attr_reader :sleep_window_seconds
|
20
|
+
|
21
|
+
# number of requests that must be made within a statistical window before
|
22
|
+
# open/close decisions are made using stats
|
23
|
+
attr_reader :request_volume_threshold
|
24
|
+
|
25
|
+
# % of "marks" that must be failed to trip the circuit
|
26
|
+
attr_reader :error_threshold_percentage
|
27
|
+
|
28
|
+
# number of seconds in the statistical window
|
29
|
+
attr_reader :window_size_in_seconds
|
30
|
+
|
31
|
+
# size of buckets in statistical window
|
32
|
+
attr_reader :bucket_size_in_seconds
|
33
|
+
|
34
|
+
def initialize(options = {})
|
35
|
+
@force_open = options.fetch(:force_open, false)
|
36
|
+
@force_closed = options.fetch(:force_closed, false)
|
37
|
+
@instrumenter = options.fetch(:instrumenter, Instrumenters::Noop)
|
38
|
+
@sleep_window_seconds = options.fetch(:sleep_window_seconds, 5)
|
39
|
+
@request_volume_threshold = options.fetch(:request_volume_threshold, 20)
|
40
|
+
@error_threshold_percentage = options.fetch(:error_threshold_percentage, 50)
|
41
|
+
@window_size_in_seconds = options.fetch(:window_size_in_seconds, 60)
|
42
|
+
@bucket_size_in_seconds = options.fetch(:bucket_size_in_seconds, 10)
|
43
|
+
|
44
|
+
if @bucket_size_in_seconds >= @window_size_in_seconds
|
45
|
+
raise ArgumentError, "bucket_size_in_seconds must be smaller than window_size_in_seconds"
|
46
|
+
end
|
47
|
+
|
48
|
+
if @window_size_in_seconds % @bucket_size_in_seconds != 0
|
49
|
+
raise ArgumentError,
|
50
|
+
"window_size_in_seconds must be perfectly divisible by" +
|
51
|
+
" bucket_size_in_seconds in order to evenly partition the buckets"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -1,30 +1,33 @@
|
|
1
1
|
module Resilient
|
2
2
|
class Test
|
3
3
|
module CircuitBreakerInterface
|
4
|
+
def test_responds_to_key
|
5
|
+
assert_respond_to @object, :key
|
6
|
+
end
|
7
|
+
|
4
8
|
def test_responds_to_allow_request
|
5
9
|
assert_respond_to @object, :allow_request?
|
6
10
|
end
|
7
11
|
|
8
|
-
def
|
9
|
-
assert_respond_to @object, :
|
12
|
+
def test_responds_to_success
|
13
|
+
assert_respond_to @object, :success
|
10
14
|
end
|
11
15
|
|
12
|
-
def
|
13
|
-
assert_nil @object.
|
16
|
+
def test_success_returns_nothing
|
17
|
+
assert_nil @object.success
|
14
18
|
end
|
15
19
|
|
16
|
-
def
|
17
|
-
assert_respond_to @object, :
|
20
|
+
def test_responds_to_failure
|
21
|
+
assert_respond_to @object, :failure
|
18
22
|
end
|
19
23
|
|
20
|
-
def
|
21
|
-
assert_nil @object.
|
24
|
+
def test_failure_returns_nothing
|
25
|
+
assert_nil @object.failure
|
22
26
|
end
|
23
27
|
|
24
28
|
def test_responds_to_reset
|
25
29
|
assert_respond_to @object, :reset
|
26
30
|
end
|
27
31
|
end
|
28
|
-
|
29
32
|
end
|
30
33
|
end
|
@@ -1,20 +1,20 @@
|
|
1
1
|
module Resilient
|
2
2
|
class Test
|
3
3
|
module MetricsInterface
|
4
|
-
def
|
5
|
-
assert_respond_to @object, :
|
4
|
+
def test_responds_to_success
|
5
|
+
assert_respond_to @object, :success
|
6
6
|
end
|
7
7
|
|
8
|
-
def
|
9
|
-
assert_nil @object.
|
8
|
+
def test_success_returns_nothing
|
9
|
+
assert_nil @object.success
|
10
10
|
end
|
11
11
|
|
12
|
-
def
|
13
|
-
assert_respond_to @object, :
|
12
|
+
def test_responds_to_failure
|
13
|
+
assert_respond_to @object, :failure
|
14
14
|
end
|
15
15
|
|
16
|
-
def
|
17
|
-
assert_nil @object.
|
16
|
+
def test_failure_returns_nothing
|
17
|
+
assert_nil @object.failure
|
18
18
|
end
|
19
19
|
|
20
20
|
def test_responds_to_successes
|
@@ -5,10 +5,6 @@ module Resilient
|
|
5
5
|
assert_respond_to @object, :increment
|
6
6
|
end
|
7
7
|
|
8
|
-
def test_responds_to_get
|
9
|
-
assert_respond_to @object, :get
|
10
|
-
end
|
11
|
-
|
12
8
|
def test_responds_to_sum
|
13
9
|
assert_respond_to @object, :sum
|
14
10
|
end
|
@@ -17,10 +13,6 @@ module Resilient
|
|
17
13
|
assert_respond_to @object, :prune
|
18
14
|
end
|
19
15
|
|
20
|
-
def test_responds_to_reset
|
21
|
-
assert_respond_to @object, :reset
|
22
|
-
end
|
23
|
-
|
24
16
|
def test_increment
|
25
17
|
buckets = [
|
26
18
|
Resilient::CircuitBreaker::Metrics::Bucket.new(1, 5),
|
@@ -37,7 +29,7 @@ module Resilient
|
|
37
29
|
assert_equal 1, @object.source[buckets[1]][:failures]
|
38
30
|
end
|
39
31
|
|
40
|
-
def
|
32
|
+
def test_sum_defaults
|
41
33
|
buckets = [
|
42
34
|
Resilient::CircuitBreaker::Metrics::Bucket.new(1, 5),
|
43
35
|
Resilient::CircuitBreaker::Metrics::Bucket.new(6, 10),
|
@@ -46,33 +38,12 @@ module Resilient
|
|
46
38
|
:successes,
|
47
39
|
:failures,
|
48
40
|
]
|
49
|
-
result = @object.
|
50
|
-
assert_equal 0, result[
|
51
|
-
assert_equal 0, result[
|
52
|
-
assert_equal 0, result[buckets[1]][:successes]
|
53
|
-
assert_equal 0, result[buckets[1]][:failures]
|
54
|
-
end
|
55
|
-
|
56
|
-
def test_get_with_values
|
57
|
-
buckets = [
|
58
|
-
Resilient::CircuitBreaker::Metrics::Bucket.new(1, 5),
|
59
|
-
Resilient::CircuitBreaker::Metrics::Bucket.new(6, 10),
|
60
|
-
]
|
61
|
-
keys = [
|
62
|
-
:successes,
|
63
|
-
:failures,
|
64
|
-
]
|
65
|
-
@object.increment(buckets, keys)
|
66
|
-
@object.increment(buckets, keys)
|
67
|
-
@object.increment(buckets[0], keys)
|
68
|
-
result = @object.get(buckets, keys)
|
69
|
-
assert_equal 3, result[buckets[0]][:successes]
|
70
|
-
assert_equal 3, result[buckets[0]][:failures]
|
71
|
-
assert_equal 2, result[buckets[1]][:successes]
|
72
|
-
assert_equal 2, result[buckets[1]][:failures]
|
41
|
+
result = @object.sum(buckets, keys)
|
42
|
+
assert_equal 0, result[:successes]
|
43
|
+
assert_equal 0, result[:failures]
|
73
44
|
end
|
74
45
|
|
75
|
-
def
|
46
|
+
def test_sum_with_values
|
76
47
|
buckets = [
|
77
48
|
Resilient::CircuitBreaker::Metrics::Bucket.new(1, 5),
|
78
49
|
Resilient::CircuitBreaker::Metrics::Bucket.new(6, 10),
|
@@ -98,26 +69,6 @@ module Resilient
|
|
98
69
|
assert_equal 4, @object.sum(buckets[1], keys).values.inject(0) { |sum, value| sum += value }
|
99
70
|
end
|
100
71
|
|
101
|
-
def test_reset
|
102
|
-
buckets = [
|
103
|
-
Resilient::CircuitBreaker::Metrics::Bucket.new(1, 5),
|
104
|
-
Resilient::CircuitBreaker::Metrics::Bucket.new(6, 10),
|
105
|
-
]
|
106
|
-
keys = [
|
107
|
-
:successes,
|
108
|
-
:failures,
|
109
|
-
]
|
110
|
-
@object.increment(buckets, keys)
|
111
|
-
@object.increment(buckets, keys)
|
112
|
-
@object.increment(buckets[0], keys)
|
113
|
-
@object.reset(buckets, keys)
|
114
|
-
result = @object.get(buckets, keys)
|
115
|
-
assert_equal 0, result[buckets[0]][:successes]
|
116
|
-
assert_equal 0, result[buckets[0]][:failures]
|
117
|
-
assert_equal 0, result[buckets[1]][:successes]
|
118
|
-
assert_equal 0, result[buckets[1]][:failures]
|
119
|
-
end
|
120
|
-
|
121
72
|
def test_prune
|
122
73
|
buckets = [
|
123
74
|
Resilient::CircuitBreaker::Metrics::Bucket.new(1, 5),
|
@@ -131,11 +82,9 @@ module Resilient
|
|
131
82
|
@object.increment(buckets, keys)
|
132
83
|
@object.increment(buckets[0], keys)
|
133
84
|
@object.prune(buckets, keys)
|
134
|
-
result = @object.
|
135
|
-
assert_equal 0, result[
|
136
|
-
assert_equal 0, result[
|
137
|
-
assert_equal 0, result[buckets[1]][:successes]
|
138
|
-
assert_equal 0, result[buckets[1]][:failures]
|
85
|
+
result = @object.sum(buckets, keys)
|
86
|
+
assert_equal 0, result[:successes]
|
87
|
+
assert_equal 0, result[:failures]
|
139
88
|
end
|
140
89
|
end
|
141
90
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
module Resilient
|
2
2
|
class Test
|
3
|
-
module
|
3
|
+
module PropertiesInterface
|
4
4
|
def test_responds_to_force_open
|
5
5
|
assert_respond_to @object, :force_open
|
6
6
|
end
|
@@ -25,10 +25,6 @@ module Resilient
|
|
25
25
|
assert_respond_to @object, :error_threshold_percentage
|
26
26
|
end
|
27
27
|
|
28
|
-
def test_responds_to_number_of_buckets
|
29
|
-
assert_respond_to @object, :number_of_buckets
|
30
|
-
end
|
31
|
-
|
32
28
|
def test_responds_to_bucket_size_in_seconds
|
33
29
|
assert_respond_to @object, :bucket_size_in_seconds
|
34
30
|
end
|
data/lib/resilient/version.rb
CHANGED
data/resilient.gemspec
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# coding: utf-8
|
2
|
-
lib = File.expand_path(
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
3
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
-
require
|
4
|
+
require "resilient/version"
|
5
5
|
|
6
6
|
Gem::Specification.new do |spec|
|
7
7
|
spec.name = "resilient"
|
@@ -19,6 +19,6 @@ Gem::Specification.new do |spec|
|
|
19
19
|
spec.require_paths = ["lib"]
|
20
20
|
|
21
21
|
spec.add_development_dependency "bundler", "~> 1.10"
|
22
|
-
spec.add_development_dependency "rake", "~> 10.0"
|
23
22
|
spec.add_development_dependency "minitest", "~> 5.8"
|
23
|
+
spec.add_development_dependency "timecop", "~> 0.8.0"
|
24
24
|
end
|
data/script/bootstrap
CHANGED
@@ -1 +1,15 @@
|
|
1
|
+
#!/bin/sh
|
2
|
+
#/ Usage: bootstrap
|
3
|
+
#/
|
4
|
+
#/ Get the dependencies needed to work on the gem.
|
5
|
+
#/
|
6
|
+
|
7
|
+
set -e
|
8
|
+
cd $(dirname "$0")/..
|
9
|
+
|
10
|
+
[ "$1" = "--help" -o "$1" = "-h" -o "$1" = "help" ] && {
|
11
|
+
grep '^#/' <"$0"| cut -c4-
|
12
|
+
exit 0
|
13
|
+
}
|
14
|
+
|
1
15
|
bundle --quiet
|
data/script/watch
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: resilient
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- John Nunemaker
|
@@ -14,44 +14,44 @@ dependencies:
|
|
14
14
|
name: bundler
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- -
|
17
|
+
- - ~>
|
18
18
|
- !ruby/object:Gem::Version
|
19
19
|
version: '1.10'
|
20
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
26
|
version: '1.10'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
|
-
name:
|
28
|
+
name: minitest
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- -
|
31
|
+
- - ~>
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '
|
33
|
+
version: '5.8'
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- -
|
38
|
+
- - ~>
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: '
|
40
|
+
version: '5.8'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
|
-
name:
|
42
|
+
name: timecop
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
|
-
- -
|
45
|
+
- - ~>
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version:
|
47
|
+
version: 0.8.0
|
48
48
|
type: :development
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
|
-
- -
|
52
|
+
- - ~>
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version:
|
54
|
+
version: 0.8.0
|
55
55
|
description:
|
56
56
|
email:
|
57
57
|
- nunemaker@gmail.com
|
@@ -59,26 +59,30 @@ executables: []
|
|
59
59
|
extensions: []
|
60
60
|
extra_rdoc_files: []
|
61
61
|
files:
|
62
|
-
-
|
63
|
-
-
|
62
|
+
- .gitignore
|
63
|
+
- .travis.yml
|
64
64
|
- Gemfile
|
65
65
|
- Guardfile
|
66
66
|
- LICENSE.txt
|
67
67
|
- README.md
|
68
|
-
- Rakefile
|
69
68
|
- examples/basic.rb
|
70
69
|
- examples/long_running.rb
|
71
70
|
- lib/resilient.rb
|
72
71
|
- lib/resilient/circuit_breaker.rb
|
73
|
-
- lib/resilient/circuit_breaker/config.rb
|
74
72
|
- lib/resilient/circuit_breaker/metrics.rb
|
73
|
+
- lib/resilient/circuit_breaker/metrics/bucket.rb
|
74
|
+
- lib/resilient/circuit_breaker/metrics/bucket_range.rb
|
75
|
+
- lib/resilient/circuit_breaker/metrics/bucket_size.rb
|
75
76
|
- lib/resilient/circuit_breaker/metrics/storage/memory.rb
|
77
|
+
- lib/resilient/circuit_breaker/metrics/window_size.rb
|
78
|
+
- lib/resilient/circuit_breaker/properties.rb
|
76
79
|
- lib/resilient/instrumenters/memory.rb
|
77
80
|
- lib/resilient/instrumenters/noop.rb
|
81
|
+
- lib/resilient/key.rb
|
78
82
|
- lib/resilient/test/circuit_breaker_interface.rb
|
79
|
-
- lib/resilient/test/config_interface.rb
|
80
83
|
- lib/resilient/test/metrics_interface.rb
|
81
84
|
- lib/resilient/test/metrics_storage_interface.rb
|
85
|
+
- lib/resilient/test/properties_interface.rb
|
82
86
|
- lib/resilient/version.rb
|
83
87
|
- resilient.gemspec
|
84
88
|
- script/bootstrap
|
@@ -96,17 +100,17 @@ require_paths:
|
|
96
100
|
- lib
|
97
101
|
required_ruby_version: !ruby/object:Gem::Requirement
|
98
102
|
requirements:
|
99
|
-
- -
|
103
|
+
- - '>='
|
100
104
|
- !ruby/object:Gem::Version
|
101
105
|
version: '0'
|
102
106
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
103
107
|
requirements:
|
104
|
-
- -
|
108
|
+
- - '>='
|
105
109
|
- !ruby/object:Gem::Version
|
106
110
|
version: '0'
|
107
111
|
requirements: []
|
108
112
|
rubyforge_project:
|
109
|
-
rubygems_version: 2.
|
113
|
+
rubygems_version: 2.0.3
|
110
114
|
signing_key:
|
111
115
|
specification_version: 4
|
112
116
|
summary: toolkit for resilient ruby apps
|
data/Rakefile
DELETED
@@ -1,27 +0,0 @@
|
|
1
|
-
require "resilient/instrumenters/noop"
|
2
|
-
|
3
|
-
module Resilient
|
4
|
-
class CircuitBreaker
|
5
|
-
class Config
|
6
|
-
attr_reader :force_open
|
7
|
-
attr_reader :force_closed
|
8
|
-
attr_reader :instrumenter
|
9
|
-
attr_reader :sleep_window_seconds
|
10
|
-
attr_reader :request_volume_threshold
|
11
|
-
attr_reader :error_threshold_percentage
|
12
|
-
attr_reader :number_of_buckets
|
13
|
-
attr_reader :bucket_size_in_seconds
|
14
|
-
|
15
|
-
def initialize(options = {})
|
16
|
-
@force_open = options.fetch(:force_open, false)
|
17
|
-
@force_closed = options.fetch(:force_closed, false)
|
18
|
-
@instrumenter = options.fetch(:instrumenter, Instrumenters::Noop)
|
19
|
-
@sleep_window_seconds = options.fetch(:sleep_window_seconds, 5)
|
20
|
-
@request_volume_threshold = options.fetch(:request_volume_threshold, 20)
|
21
|
-
@error_threshold_percentage = options.fetch(:error_threshold_percentage, 50)
|
22
|
-
@number_of_buckets = options.fetch(:number_of_buckets, 6)
|
23
|
-
@bucket_size_in_seconds = options.fetch(:bucket_size_in_seconds, 10)
|
24
|
-
end
|
25
|
-
end
|
26
|
-
end
|
27
|
-
end
|