resilient 0.1.0 → 0.2.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: 4d96ef8b25a0a297da28e4c1f718ab826350a23d
4
- data.tar.gz: 8bdf3b03abfedbcf9cd6531a7aad4cd2b3313a18
3
+ metadata.gz: 5cebeacad55c7a80623379752b452f9a20dd0009
4
+ data.tar.gz: 9d56f7dc8b598304b8f2aea2e6c46a743f0820f2
5
5
  SHA512:
6
- metadata.gz: 067674abbd47f23741bcc38ca4c99e0ddbcfb187d4e25b84e04841e0055a34ada9e368350868de7fc5363f811e8a20631fcd7d5e13c490918b86242c3f9154f5
7
- data.tar.gz: 90c8672939280363a2da23664a4ebadf5e413ce40cf08b38130e00bea1923ac5971c126f2bba5b9c4417e639f16b89e9c92d1b2289e97af3662074e1719c549f
6
+ metadata.gz: 63d8cb0a2f6d784573a22df49b407530ebbbe8e8cc534c678ff92d89f749e62ab9c906eb17ad490aad92ede7bf5f8bbeffa0a8e265f801b29a20131804d28647
7
+ data.tar.gz: 02023b4a9de5bb484fb732c28e11538aa33f0a6ee01e84bff4d1f5c85ae483886e6d1171af99a86eeebbaa5256909f0cce0a4213e4f6d24144e330566b0c6374
data/.gitignore CHANGED
@@ -7,3 +7,4 @@
7
7
  /pkg/
8
8
  /spec/reports/
9
9
  /tmp/
10
+ *.gem
data/Gemfile CHANGED
@@ -1,8 +1,5 @@
1
1
  source "https://rubygems.org"
2
-
3
- # Specify your gem's dependencies in resilient.gemspec
4
2
  gemspec
5
3
 
6
4
  gem "guard", "~> 2.13.0"
7
5
  gem "guard-minitest", "~> 2.4.4"
8
- gem "timecop", "0.8.0"
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 for resilient in ruby. For now, just a circuit breaker (~~stolen from~~ based on [hystrix](https://github.com/netflix/hystrix)). Soon much more...
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 config for circuit
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.mark_success
33
+ circuit_breaker.success
34
34
  rescue => boom
35
- circuit_breaker.mark_failure
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 config of circuit:
43
+ customize properties of circuit:
44
44
 
45
45
  ```ruby
46
- config = Resilient::CircuitBreaker::Config.new({
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(config: config)
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
- config = Resilient::CircuitBreaker::Config.new(force_open: true)
62
- circuit_breaker = Resilient::CircuitBreaker.new(config: config)
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
- config = Resilient::CircuitBreaker::Config.new(force_closed: true)
70
- circuit_breaker = Resilient::CircuitBreaker.new(config: config)
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
- number_of_buckets: 10,
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
- config = Resilient::CircuitBreaker::Config.new({
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(config: config)
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.mark_success
22
+ circuit_breaker.success
23
23
  rescue => boom
24
24
  # won't get here in this example
25
- circuit_breaker.mark_failure
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.mark_failure
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.config.request_volume_threshold.times do
46
- circuit_breaker.mark_failure
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.mark_success
75
+ circuit_breaker.success
76
76
 
77
77
  if circuit_breaker.allow_request?
78
78
  puts "circuit reset and back closed now, allowing requests"
@@ -8,14 +8,14 @@ $:.unshift(lib_path)
8
8
  require "pp"
9
9
  require "resilient/circuit_breaker"
10
10
 
11
- config = Resilient::CircuitBreaker::Config.new({
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
- number_of_buckets: 10,
15
+ window_size_in_seconds: 60,
16
16
  bucket_size_in_seconds: 1,
17
17
  })
18
- circuit_breaker = Resilient::CircuitBreaker.new(config: config)
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.mark_success
27
+ circuit_breaker.success
28
28
  rescue => boom
29
29
  puts "request failed"
30
- circuit_breaker.mark_failure
30
+ circuit_breaker.failure
31
31
  end
32
32
  else
33
33
  puts "request denied"
data/lib/resilient.rb CHANGED
@@ -2,3 +2,5 @@ require "resilient/version"
2
2
 
3
3
  module Resilient
4
4
  end
5
+
6
+ require "resilient/circuit_breaker"
@@ -1,40 +1,52 @@
1
+ require "resilient/key"
1
2
  require "resilient/circuit_breaker/metrics"
2
- require "resilient/circuit_breaker/config"
3
+ require "resilient/circuit_breaker/properties"
3
4
 
4
5
  module Resilient
5
6
  class CircuitBreaker
6
- attr_reader :metrics
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, config: Config.new, metrics: Metrics.new)
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
- @config = config
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
- number_of_buckets: config.number_of_buckets,
20
- bucket_size_in_seconds: config.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] = @config.force_open
44
+ result = if payload[:force_open] = @properties.force_open
33
45
  false
34
46
  else
35
- if payload[:force_closed] = @config.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 config in
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 mark_success
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.mark_success", default_payload) { |payload|
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.mark_success
80
+ @metrics.success
68
81
  end
69
82
  nil
70
83
  }
71
84
  end
72
85
 
73
- def mark_failure
74
- instrument("resilient.circuit_breaker.mark_failure") { |payload|
75
- @metrics.mark_failure
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
- instrument("resilient.circuit_breaker.reset") { |payload|
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 < @config.request_volume_threshold
124
+ @metrics.requests < @properties.request_volume_threshold
104
125
  end
105
126
 
106
127
  def under_error_threshold_percentage?
107
- @metrics.error_percentage < @config.error_threshold_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 + @config.sleep_window_seconds)
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
- config.instrumenter.instrument(name, payload, &block)
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 :number_of_buckets
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
- @number_of_buckets = options.fetch(:number_of_buckets, 6)
81
- @bucket_size = BucketSize.new(options.fetch(:bucket_size_in_seconds, 10))
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 mark_success
33
+ def success
87
34
  @storage.increment(current_bucket, StorageSuccessKeys)
88
35
  prune_buckets
89
36
  nil
90
37
  end
91
38
 
92
- def mark_failure
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.reset(@buckets, StorageKeys)
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, @number_of_buckets, @bucket_size)
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 reset(buckets, keys)
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,15 @@
1
+ require "resilient/circuit_breaker/metrics/bucket"
2
+
3
+ module Resilient
4
+ class CircuitBreaker
5
+ class Metrics
6
+ class WindowSize
7
+ attr_reader :seconds
8
+
9
+ def initialize(seconds)
10
+ @seconds = seconds
11
+ end
12
+ end
13
+ end
14
+ end
15
+ 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
@@ -0,0 +1,9 @@
1
+ module Resilient
2
+ class Key
3
+ attr_reader :name
4
+
5
+ def initialize(name)
6
+ @name = name
7
+ end
8
+ end
9
+ 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 test_responds_to_mark_success
9
- assert_respond_to @object, :mark_success
12
+ def test_responds_to_success
13
+ assert_respond_to @object, :success
10
14
  end
11
15
 
12
- def test_mark_success_returns_nothing
13
- assert_nil @object.mark_success
16
+ def test_success_returns_nothing
17
+ assert_nil @object.success
14
18
  end
15
19
 
16
- def test_responds_to_mark_failure
17
- assert_respond_to @object, :mark_failure
20
+ def test_responds_to_failure
21
+ assert_respond_to @object, :failure
18
22
  end
19
23
 
20
- def test_mark_failure_returns_nothing
21
- assert_nil @object.mark_failure
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 test_responds_to_mark_success
5
- assert_respond_to @object, :mark_success
4
+ def test_responds_to_success
5
+ assert_respond_to @object, :success
6
6
  end
7
7
 
8
- def test_mark_success_returns_nothing
9
- assert_nil @object.mark_success
8
+ def test_success_returns_nothing
9
+ assert_nil @object.success
10
10
  end
11
11
 
12
- def test_responds_to_mark_failure
13
- assert_respond_to @object, :mark_failure
12
+ def test_responds_to_failure
13
+ assert_respond_to @object, :failure
14
14
  end
15
15
 
16
- def test_mark_failure_returns_nothing
17
- assert_nil @object.mark_failure
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 test_get_defaults
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.get(buckets, keys)
50
- assert_equal 0, result[buckets[0]][:successes]
51
- assert_equal 0, result[buckets[0]][:failures]
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 test_sum
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.get(buckets, keys)
135
- assert_equal 0, result[buckets[0]][:successes]
136
- assert_equal 0, result[buckets[0]][:failures]
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 ConfigInterface
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
@@ -1,3 +1,3 @@
1
1
  module Resilient
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
data/resilient.gemspec CHANGED
@@ -1,7 +1,7 @@
1
1
  # coding: utf-8
2
- lib = File.expand_path('../lib', __FILE__)
2
+ lib = File.expand_path("../lib", __FILE__)
3
3
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require 'resilient/version'
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
@@ -1 +1,15 @@
1
+ #!/bin/sh
2
+ #/ Usage: watch
3
+ #/
4
+ #/ Startup guard.
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 exec guard
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.1.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: rake
28
+ name: minitest
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - "~>"
31
+ - - ~>
32
32
  - !ruby/object:Gem::Version
33
- version: '10.0'
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: '10.0'
40
+ version: '5.8'
41
41
  - !ruby/object:Gem::Dependency
42
- name: minitest
42
+ name: timecop
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - "~>"
45
+ - - ~>
46
46
  - !ruby/object:Gem::Version
47
- version: '5.8'
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: '5.8'
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
- - ".gitignore"
63
- - ".travis.yml"
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.2.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,10 +0,0 @@
1
- require "bundler/gem_tasks"
2
- require "rake/testtask"
3
-
4
- Rake::TestTask.new(:test) do |t|
5
- t.libs << "test"
6
- t.libs << "lib"
7
- t.test_files = FileList['test/**/*_test.rb']
8
- end
9
-
10
- task :default => :test
@@ -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