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