resilient 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 822421a58f373992d96937a055b287a2f633ef1b
4
- data.tar.gz: 6e29f01491cce3b1244998c9dc38758db56bc58d
3
+ metadata.gz: 4d96ef8b25a0a297da28e4c1f718ab826350a23d
4
+ data.tar.gz: 8bdf3b03abfedbcf9cd6531a7aad4cd2b3313a18
5
5
  SHA512:
6
- metadata.gz: d6952a74ffed454c5c1909991db5dfd056dfb5f26932bc1537776cee363dcc55cbeff6b549da1ac112f124437032b92d24c071e516a9702bc72bd747187a3522
7
- data.tar.gz: 9d1edae9fb8f13856fc988af0b257a7d2d2e184a04068e5dc1f054d945666c49cbe934ee5ff23e8a983ed074b27f527f5258bdcfced828ef420434bfd5669d8b
6
+ metadata.gz: 067674abbd47f23741bcc38ca4c99e0ddbcfb187d4e25b84e04841e0055a34ada9e368350868de7fc5363f811e8a20631fcd7d5e13c490918b86242c3f9154f5
7
+ data.tar.gz: 90c8672939280363a2da23664a4ebadf5e413ce40cf08b38130e00bea1923ac5971c126f2bba5b9c4417e639f16b89e9c92d1b2289e97af3662074e1719c549f
data/.travis.yml CHANGED
@@ -1,4 +1,7 @@
1
1
  language: ruby
2
2
  rvm:
3
+ - 2.0.0
3
4
  - 2.1.4
5
+ - 2.2.3
4
6
  before_install: gem install bundler -v 1.10.6
7
+ script: script/test
data/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # Resilient
2
2
 
3
- Some tools for resilient in ruby.
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...
4
+
5
+ Nothing asynchronous or thread safe yet either.
4
6
 
5
7
  ## Installation
6
8
 
@@ -25,13 +27,13 @@ require "resilient/circuit_breaker"
25
27
 
26
28
  # default config for circuit
27
29
  circuit_breaker = Resilient::CircuitBreaker.new
28
- if circuit_breaker.request_allowed?
30
+ if circuit_breaker.allow_request?
29
31
  begin
30
32
  # do something expensive
31
33
  circuit_breaker.mark_success
32
34
  rescue => boom
33
- # do fallback
34
35
  circuit_breaker.mark_failure
36
+ # do fallback
35
37
  end
36
38
  else
37
39
  # do fallback
@@ -41,7 +43,7 @@ end
41
43
  customize config of circuit:
42
44
 
43
45
  ```ruby
44
- config = Resilient::CircuitBreaker::RollingConfig.new({
46
+ config = Resilient::CircuitBreaker::Config.new({
45
47
  # at what percentage of errors should we open the circuit
46
48
  error_threshold_percentage: 50,
47
49
  # do not try request again for 5 seconds
@@ -56,7 +58,7 @@ circuit_breaker = Resilient::CircuitBreaker.new(config: config)
56
58
  force the circuit to be always open:
57
59
 
58
60
  ```ruby
59
- config = Resilient::CircuitBreaker::RollingConfig.new(force_open: true)
61
+ config = Resilient::CircuitBreaker::Config.new(force_open: true)
60
62
  circuit_breaker = Resilient::CircuitBreaker.new(config: config)
61
63
  # etc etc etc
62
64
  ```
@@ -64,7 +66,7 @@ circuit_breaker = Resilient::CircuitBreaker.new(config: config)
64
66
  force the circuit to be always closed:
65
67
 
66
68
  ```ruby
67
- config = Resilient::CircuitBreaker::RollingConfig.new(force_closed: true)
69
+ config = Resilient::CircuitBreaker::Config.new(force_closed: true)
68
70
  circuit_breaker = Resilient::CircuitBreaker.new(config: config)
69
71
  # etc etc etc
70
72
  ```
@@ -72,7 +74,7 @@ circuit_breaker = Resilient::CircuitBreaker.new(config: config)
72
74
  customize rolling window to be 10 buckets of 1 second each (10 seconds in all):
73
75
 
74
76
  ```ruby
75
- metrics = Resilient::CircuitBreaker::RollingMetrics.new({
77
+ metrics = Resilient::CircuitBreaker::Metrics.new({
76
78
  number_of_buckets: 10,
77
79
  bucket_size_in_seconds: 1,
78
80
  })
data/examples/basic.rb ADDED
@@ -0,0 +1,81 @@
1
+ # setting up load path
2
+ require "pathname"
3
+ root_path = Pathname(__FILE__).dirname.join("..").expand_path
4
+ lib_path = root_path.join("lib")
5
+ $:.unshift(lib_path)
6
+
7
+ # requiring stuff for this example
8
+ require "pp"
9
+ require "resilient/circuit_breaker"
10
+
11
+ config = Resilient::CircuitBreaker::Config.new({
12
+ sleep_window_seconds: 1,
13
+ request_volume_threshold: 10,
14
+ error_threshold_percentage: 25,
15
+ })
16
+ circuit_breaker = Resilient::CircuitBreaker.new(config: config)
17
+
18
+ # success
19
+ if circuit_breaker.allow_request?
20
+ begin
21
+ puts "do expensive thing"
22
+ circuit_breaker.mark_success
23
+ rescue => boom
24
+ # won't get here in this example
25
+ circuit_breaker.mark_failure
26
+ end
27
+ else
28
+ raise "will not get here"
29
+ end
30
+
31
+ # failure
32
+ if circuit_breaker.allow_request?
33
+ begin
34
+ raise
35
+ rescue => boom
36
+ circuit_breaker.mark_failure
37
+ puts "failed slow, do fallback"
38
+ end
39
+ else
40
+ raise "will not get here"
41
+ end
42
+
43
+ # trip circuit, imagine this being same as above but in real life...
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
47
+ end
48
+
49
+ # fail fast
50
+ if circuit_breaker.allow_request?
51
+ raise "will not get here"
52
+ else
53
+ puts "failed fast, do fallback"
54
+ end
55
+
56
+ now = Time.now
57
+
58
+ while Time.now - now < 3
59
+ if circuit_breaker.allow_request?
60
+ puts "doing a single attempt as we've failed fast for sleep_window_seconds"
61
+ break
62
+ else
63
+ puts "failed fast, do fallback"
64
+ end
65
+ sleep rand(0.1)
66
+ end
67
+
68
+ if circuit_breaker.allow_request?
69
+ raise "will not get here"
70
+ else
71
+ puts "request denied because single request has not been marked success yet"
72
+ end
73
+
74
+ puts "marking single request as success"
75
+ circuit_breaker.mark_success
76
+
77
+ if circuit_breaker.allow_request?
78
+ puts "circuit reset and back closed now, allowing requests"
79
+ else
80
+ raise "will not get here"
81
+ end
@@ -0,0 +1,42 @@
1
+ # setting up load path
2
+ require "pathname"
3
+ root_path = Pathname(__FILE__).dirname.join("..").expand_path
4
+ lib_path = root_path.join("lib")
5
+ $:.unshift(lib_path)
6
+
7
+ # requiring stuff for this example
8
+ require "pp"
9
+ require "resilient/circuit_breaker"
10
+
11
+ config = Resilient::CircuitBreaker::Config.new({
12
+ sleep_window_seconds: 5,
13
+ request_volume_threshold: 20,
14
+ error_threshold_percentage: 10,
15
+ number_of_buckets: 10,
16
+ bucket_size_in_seconds: 1,
17
+ })
18
+ circuit_breaker = Resilient::CircuitBreaker.new(config: config)
19
+
20
+ iterations = 0
21
+ loop do
22
+ if circuit_breaker.allow_request?
23
+ begin
24
+ puts "request allowed"
25
+ raise if rand(100) < 10
26
+ puts "request succeeded"
27
+ circuit_breaker.mark_success
28
+ rescue => boom
29
+ puts "request failed"
30
+ circuit_breaker.mark_failure
31
+ end
32
+ else
33
+ puts "request denied"
34
+ end
35
+ puts "\n"
36
+ sleep 0.1
37
+ iterations += 1
38
+
39
+ if iterations % 10 == 0
40
+ p successes: circuit_breaker.metrics.successes, failures: circuit_breaker.metrics.failures, error_percentage: circuit_breaker.metrics.error_percentage, buckets: circuit_breaker.metrics.buckets.length
41
+ end
42
+ end
@@ -1,5 +1,5 @@
1
- require "resilient/circuit_breaker/rolling_metrics"
2
- require "resilient/circuit_breaker/rolling_config"
1
+ require "resilient/circuit_breaker/metrics"
2
+ require "resilient/circuit_breaker/config"
3
3
 
4
4
  module Resilient
5
5
  class CircuitBreaker
@@ -8,14 +8,14 @@ module Resilient
8
8
  attr_reader :open
9
9
  attr_reader :opened_or_last_checked_at_epoch
10
10
 
11
- def initialize(open: false, config: RollingConfig.new, metrics: RollingMetrics.new)
11
+ def initialize(open: false, config: Config.new, metrics: Metrics.new)
12
12
  @open = open
13
13
  @opened_or_last_checked_at_epoch = 0
14
14
  @config = config
15
15
  @metrics = if metrics
16
16
  metrics
17
17
  else
18
- RollingMetrics.new({
18
+ Metrics.new({
19
19
  number_of_buckets: config.number_of_buckets,
20
20
  bucket_size_in_seconds: config.bucket_size_in_seconds,
21
21
  })
@@ -23,32 +23,74 @@ module Resilient
23
23
  end
24
24
 
25
25
  def allow_request?
26
- return false if @config.force_open
27
- return true if @config.force_closed
28
-
29
- closed? || allow_single_request?
26
+ default_payload = {
27
+ force_open: false,
28
+ force_closed: false,
29
+ }
30
+
31
+ instrument("resilient.circuit_breaker.allow_request", default_payload) { |payload|
32
+ result = if payload[:force_open] = @config.force_open
33
+ false
34
+ else
35
+ if payload[:force_closed] = @config.force_closed
36
+ # we still want to simulate normal behavior/metrics like open, allow
37
+ # single request, etc. so it is possible to test config in
38
+ # production without impact
39
+ if payload[:open] = open?
40
+ payload[:allow_single_request] = allow_single_request?
41
+ end
42
+
43
+ true
44
+ else
45
+ if !(payload[:open] = open?)
46
+ true
47
+ else
48
+ payload[:allow_single_request] = allow_single_request?
49
+ end
50
+ end
51
+ end
52
+
53
+ payload[:result] = result
54
+ }
30
55
  end
31
56
 
32
57
  def mark_success
33
- close_circuit if @open
58
+ default_payload = {
59
+ closed_the_circuit: false,
60
+ }
61
+
62
+ instrument("resilient.circuit_breaker.mark_success", default_payload) { |payload|
63
+ if @open
64
+ payload[:closed_the_circuit] = true
65
+ close_circuit
66
+ else
67
+ @metrics.mark_success
68
+ end
69
+ nil
70
+ }
34
71
  end
35
72
 
36
73
  def mark_failure
37
- @metrics.mark_failure
74
+ instrument("resilient.circuit_breaker.mark_failure") { |payload|
75
+ @metrics.mark_failure
76
+ nil
77
+ }
38
78
  end
39
79
 
40
80
  def reset
41
- @open = false
42
- @opened_or_last_checked_at_epoch = 0
43
- @metrics.reset
44
- nil
81
+ instrument("resilient.circuit_breaker.reset") { |payload|
82
+ @open = false
83
+ @opened_or_last_checked_at_epoch = 0
84
+ @metrics.reset
85
+ nil
86
+ }
45
87
  end
46
88
 
47
89
  private
48
90
 
49
91
  def open_circuit
50
- @open = true
51
92
  @opened_or_last_checked_at_epoch = Time.now.to_i
93
+ @open = true
52
94
  end
53
95
 
54
96
  def close_circuit
@@ -71,6 +113,7 @@ module Resilient
71
113
  return false if under_error_threshold_percentage?
72
114
 
73
115
  open_circuit
116
+ true
74
117
  end
75
118
 
76
119
  def closed?
@@ -78,15 +121,18 @@ module Resilient
78
121
  end
79
122
 
80
123
  def allow_single_request?
81
- try_next_request_at = @opened_or_last_checked_at_epoch + @config.sleep_window_seconds
82
124
  now = Time.now.to_i
83
125
 
84
- if @open && now > try_next_request_at
85
- @opened_or_last_checked_at_epoch = now + @config.sleep_window_seconds
126
+ if @open && now > (@opened_or_last_checked_at_epoch + @config.sleep_window_seconds)
127
+ @opened_or_last_checked_at_epoch = now
86
128
  true
87
129
  else
88
130
  false
89
131
  end
90
132
  end
133
+
134
+ def instrument(name, payload = {}, &block)
135
+ config.instrumenter.instrument(name, payload, &block)
136
+ end
91
137
  end
92
138
  end
@@ -1,17 +1,21 @@
1
+ require "resilient/instrumenters/noop"
2
+
1
3
  module Resilient
2
4
  class CircuitBreaker
3
- class RollingConfig
4
- attr_reader :error_threshold_percentage
5
- attr_reader :sleep_window_seconds
6
- attr_reader :request_volume_threshold
5
+ class Config
7
6
  attr_reader :force_open
8
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
9
12
  attr_reader :number_of_buckets
10
13
  attr_reader :bucket_size_in_seconds
11
14
 
12
15
  def initialize(options = {})
13
16
  @force_open = options.fetch(:force_open, false)
14
17
  @force_closed = options.fetch(:force_closed, false)
18
+ @instrumenter = options.fetch(:instrumenter, Instrumenters::Noop)
15
19
  @sleep_window_seconds = options.fetch(:sleep_window_seconds, 5)
16
20
  @request_volume_threshold = options.fetch(:request_volume_threshold, 20)
17
21
  @error_threshold_percentage = options.fetch(:error_threshold_percentage, 50)
@@ -0,0 +1,164 @@
1
+ require "resilient/circuit_breaker/metrics/storage/memory"
2
+
3
+ module Resilient
4
+ class CircuitBreaker
5
+ class Metrics
6
+ attr_reader :number_of_buckets
7
+ attr_reader :bucket_size_in_seconds
8
+ attr_reader :buckets
9
+ attr_reader :storage
10
+
11
+ StorageSuccessKeys = [
12
+ :successes,
13
+ ].freeze
14
+
15
+ StorageFailureKeys = [
16
+ :failures,
17
+ ].freeze
18
+
19
+ StorageKeys = (StorageSuccessKeys + StorageFailureKeys).freeze
20
+
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
+ 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))
82
+ @storage = options.fetch(:storage) { Storage::Memory.new }
83
+ @buckets = []
84
+ end
85
+
86
+ def mark_success
87
+ @storage.increment(current_bucket, StorageSuccessKeys)
88
+ prune_buckets
89
+ nil
90
+ end
91
+
92
+ def mark_failure
93
+ @storage.increment(current_bucket, StorageFailureKeys)
94
+ prune_buckets
95
+ nil
96
+ end
97
+
98
+ def successes
99
+ prune_buckets
100
+ @storage.sum(@buckets, :successes)[:successes]
101
+ end
102
+
103
+ def failures
104
+ prune_buckets
105
+ @storage.sum(@buckets, :failures)[:failures]
106
+ end
107
+
108
+ def requests
109
+ prune_buckets
110
+ requests = 0
111
+ @storage.sum(@buckets, StorageKeys).each do |key, value|
112
+ requests += value
113
+ end
114
+ requests
115
+ end
116
+
117
+ def error_percentage
118
+ prune_buckets
119
+
120
+ result = @storage.sum(@buckets, StorageKeys)
121
+ successes = result[:successes]
122
+ failures = result[:failures]
123
+
124
+ requests = successes + failures
125
+ return 0 if failures == 0 || requests == 0
126
+
127
+ ((failures / requests.to_f) * 100).round
128
+ end
129
+
130
+ def reset
131
+ @storage.reset(@buckets, StorageKeys)
132
+ nil
133
+ end
134
+
135
+ private
136
+
137
+ def current_bucket(timestamp = Time.now.to_i)
138
+ bucket = @buckets.detect { |bucket| bucket.include?(timestamp) }
139
+ return bucket if bucket
140
+
141
+ bucket = @bucket_size.bucket(timestamp)
142
+ @buckets.push bucket
143
+
144
+ bucket
145
+ end
146
+
147
+ def prune_buckets(timestamp = Time.now.to_i)
148
+ pruned_buckets = []
149
+ bucket_range = BucketRange.generate(timestamp, @number_of_buckets, @bucket_size)
150
+
151
+ @buckets.delete_if { |bucket|
152
+ if bucket_range.prune?(bucket)
153
+ pruned_buckets << bucket
154
+ true
155
+ end
156
+ }
157
+
158
+ if pruned_buckets.any?
159
+ @storage.prune(pruned_buckets, StorageKeys)
160
+ end
161
+ end
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,53 @@
1
+ module Resilient
2
+ class CircuitBreaker
3
+ class Metrics
4
+ module Storage
5
+ class Memory
6
+ attr_reader :source
7
+
8
+ def initialize
9
+ @source = Hash.new { |h, k| h[k] = Hash.new(0) }
10
+ end
11
+
12
+ def increment(buckets, keys)
13
+ Array(buckets).each do |bucket|
14
+ Array(keys).each do |key|
15
+ @source[bucket][key] += 1
16
+ end
17
+ end
18
+ end
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
+ def sum(buckets, keys)
31
+ response = Hash.new(0)
32
+ Array(buckets).each do |bucket|
33
+ Array(keys).each do |key|
34
+ response[key] += @source[bucket][key]
35
+ end
36
+ end
37
+ response
38
+ end
39
+
40
+ def reset(buckets, keys)
41
+ Array(buckets).each do |bucket|
42
+ @source.delete(bucket)
43
+ end
44
+ end
45
+
46
+ def prune(buckets, keys)
47
+ reset(buckets, keys)
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,30 @@
1
+ module Resilient
2
+ module Instrumenters
3
+ # Instrumentor that is useful for tests as it stores each of the events that
4
+ # are instrumented.
5
+ class Memory
6
+ Event = Struct.new(:name, :payload, :result)
7
+
8
+ attr_reader :events
9
+
10
+ def initialize
11
+ @events = []
12
+ end
13
+
14
+ def instrument(name, payload = {})
15
+ # Copy the payload to guard against later modifications to it, and to
16
+ # ensure that all instrumentation code uses the payload passed to the
17
+ # block rather than the one passed to #instrument.
18
+ payload = payload.dup
19
+
20
+ result = if block_given?
21
+ yield payload
22
+ else
23
+ nil
24
+ end
25
+ @events << Event.new(name, payload, result)
26
+ result
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,9 @@
1
+ module Resilient
2
+ module Instrumenters
3
+ class Noop
4
+ def self.instrument(name, payload = {})
5
+ yield payload if block_given?
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,30 @@
1
+ module Resilient
2
+ class Test
3
+ module CircuitBreakerInterface
4
+ def test_responds_to_allow_request
5
+ assert_respond_to @object, :allow_request?
6
+ end
7
+
8
+ def test_responds_to_mark_success
9
+ assert_respond_to @object, :mark_success
10
+ end
11
+
12
+ def test_mark_success_returns_nothing
13
+ assert_nil @object.mark_success
14
+ end
15
+
16
+ def test_responds_to_mark_failure
17
+ assert_respond_to @object, :mark_failure
18
+ end
19
+
20
+ def test_mark_failure_returns_nothing
21
+ assert_nil @object.mark_failure
22
+ end
23
+
24
+ def test_responds_to_reset
25
+ assert_respond_to @object, :reset
26
+ end
27
+ end
28
+
29
+ end
30
+ end
@@ -0,0 +1,37 @@
1
+ module Resilient
2
+ class Test
3
+ module ConfigInterface
4
+ def test_responds_to_force_open
5
+ assert_respond_to @object, :force_open
6
+ end
7
+
8
+ def test_responds_to_force_closed
9
+ assert_respond_to @object, :force_closed
10
+ end
11
+
12
+ def test_responds_to_instrumenter
13
+ assert_respond_to @object, :instrumenter
14
+ end
15
+
16
+ def test_responds_to_sleep_window_seconds
17
+ assert_respond_to @object, :sleep_window_seconds
18
+ end
19
+
20
+ def test_responds_to_request_volume_threshold
21
+ assert_respond_to @object, :request_volume_threshold
22
+ end
23
+
24
+ def test_responds_to_error_threshold_percentage
25
+ assert_respond_to @object, :error_threshold_percentage
26
+ end
27
+
28
+ def test_responds_to_number_of_buckets
29
+ assert_respond_to @object, :number_of_buckets
30
+ end
31
+
32
+ def test_responds_to_bucket_size_in_seconds
33
+ assert_respond_to @object, :bucket_size_in_seconds
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,45 @@
1
+ module Resilient
2
+ class Test
3
+ module MetricsInterface
4
+ def test_responds_to_mark_success
5
+ assert_respond_to @object, :mark_success
6
+ end
7
+
8
+ def test_mark_success_returns_nothing
9
+ assert_nil @object.mark_success
10
+ end
11
+
12
+ def test_responds_to_mark_failure
13
+ assert_respond_to @object, :mark_failure
14
+ end
15
+
16
+ def test_mark_failure_returns_nothing
17
+ assert_nil @object.mark_failure
18
+ end
19
+
20
+ def test_responds_to_successes
21
+ assert_respond_to @object, :successes
22
+ end
23
+
24
+ def test_responds_to_failures
25
+ assert_respond_to @object, :failures
26
+ end
27
+
28
+ def test_responds_to_requests
29
+ assert_respond_to @object, :requests
30
+ end
31
+
32
+ def test_responds_to_error_percentage
33
+ assert_respond_to @object, :error_percentage
34
+ end
35
+
36
+ def test_responds_to_reset
37
+ assert_respond_to @object, :reset
38
+ end
39
+
40
+ def test_reset_returns_nothing
41
+ assert_nil @object.reset
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,142 @@
1
+ module Resilient
2
+ class Test
3
+ module MetricsStorageInterface
4
+ def test_responds_to_increment
5
+ assert_respond_to @object, :increment
6
+ end
7
+
8
+ def test_responds_to_get
9
+ assert_respond_to @object, :get
10
+ end
11
+
12
+ def test_responds_to_sum
13
+ assert_respond_to @object, :sum
14
+ end
15
+
16
+ def test_responds_to_prune
17
+ assert_respond_to @object, :prune
18
+ end
19
+
20
+ def test_responds_to_reset
21
+ assert_respond_to @object, :reset
22
+ end
23
+
24
+ def test_increment
25
+ buckets = [
26
+ Resilient::CircuitBreaker::Metrics::Bucket.new(1, 5),
27
+ Resilient::CircuitBreaker::Metrics::Bucket.new(6, 10),
28
+ ]
29
+ keys = [
30
+ :successes,
31
+ :failures,
32
+ ]
33
+ @object.increment(buckets, keys)
34
+ assert_equal 1, @object.source[buckets[0]][:successes]
35
+ assert_equal 1, @object.source[buckets[0]][:failures]
36
+ assert_equal 1, @object.source[buckets[1]][:successes]
37
+ assert_equal 1, @object.source[buckets[1]][:failures]
38
+ end
39
+
40
+ def test_get_defaults
41
+ buckets = [
42
+ Resilient::CircuitBreaker::Metrics::Bucket.new(1, 5),
43
+ Resilient::CircuitBreaker::Metrics::Bucket.new(6, 10),
44
+ ]
45
+ keys = [
46
+ :successes,
47
+ :failures,
48
+ ]
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]
73
+ end
74
+
75
+ def test_sum
76
+ buckets = [
77
+ Resilient::CircuitBreaker::Metrics::Bucket.new(1, 5),
78
+ Resilient::CircuitBreaker::Metrics::Bucket.new(6, 10),
79
+ ]
80
+ keys = [
81
+ :successes,
82
+ :failures,
83
+ ]
84
+ @object.increment(buckets, keys)
85
+ @object.increment(buckets, keys)
86
+ @object.increment(buckets[0], keys)
87
+
88
+ assert_equal 5, @object.sum(buckets, [:successes])[:successes]
89
+ assert_equal 5, @object.sum(buckets, [:failures])[:failures]
90
+ assert_equal 10, @object.sum(buckets, keys).values.inject(0) { |sum, value| sum += value }
91
+
92
+ assert_equal 3, @object.sum(buckets[0], [:successes])[:successes]
93
+ assert_equal 3, @object.sum(buckets[0], [:failures])[:failures]
94
+ assert_equal 6, @object.sum(buckets[0], keys).values.inject(0) { |sum, value| sum += value }
95
+
96
+ assert_equal 2, @object.sum(buckets[1], [:successes])[:successes]
97
+ assert_equal 2, @object.sum(buckets[1], [:failures])[:failures]
98
+ assert_equal 4, @object.sum(buckets[1], keys).values.inject(0) { |sum, value| sum += value }
99
+ end
100
+
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
+ def test_prune
122
+ buckets = [
123
+ Resilient::CircuitBreaker::Metrics::Bucket.new(1, 5),
124
+ Resilient::CircuitBreaker::Metrics::Bucket.new(6, 10),
125
+ ]
126
+ keys = [
127
+ :successes,
128
+ :failures,
129
+ ]
130
+ @object.increment(buckets, keys)
131
+ @object.increment(buckets, keys)
132
+ @object.increment(buckets[0], keys)
133
+ @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]
139
+ end
140
+ end
141
+ end
142
+ end
@@ -1,3 +1,3 @@
1
1
  module Resilient
2
- VERSION = "0.0.1"
2
+ VERSION = "0.1.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: resilient
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - John Nunemaker
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2015-12-07 00:00:00.000000000 Z
11
+ date: 2015-12-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -66,12 +66,19 @@ files:
66
66
  - LICENSE.txt
67
67
  - README.md
68
68
  - Rakefile
69
- - TODO.md
69
+ - examples/basic.rb
70
+ - examples/long_running.rb
70
71
  - lib/resilient.rb
71
72
  - lib/resilient/circuit_breaker.rb
72
- - lib/resilient/circuit_breaker/rolling_config.rb
73
- - lib/resilient/circuit_breaker/rolling_metrics.rb
74
- - lib/resilient/circuit_breaker/rolling_metrics/bucket.rb
73
+ - lib/resilient/circuit_breaker/config.rb
74
+ - lib/resilient/circuit_breaker/metrics.rb
75
+ - lib/resilient/circuit_breaker/metrics/storage/memory.rb
76
+ - lib/resilient/instrumenters/memory.rb
77
+ - lib/resilient/instrumenters/noop.rb
78
+ - lib/resilient/test/circuit_breaker_interface.rb
79
+ - lib/resilient/test/config_interface.rb
80
+ - lib/resilient/test/metrics_interface.rb
81
+ - lib/resilient/test/metrics_storage_interface.rb
75
82
  - lib/resilient/version.rb
76
83
  - resilient.gemspec
77
84
  - script/bootstrap
data/TODO.md DELETED
@@ -1,4 +0,0 @@
1
- - [ ] instrumentation
2
- - [ ] add duration to mark success/failure; use it for tracking latency and for making circuit decisions
3
- - [ ] add timeout to metrics separate from failures (if we add duration)?
4
- - [ ] should force closed still instrument and all that so we can test in prod before enabling or should we allow enabling/disabling with instrumentation in a different way?
@@ -1,73 +0,0 @@
1
- require "resilient/circuit_breaker/rolling_metrics/bucket"
2
-
3
- module Resilient
4
- class CircuitBreaker
5
- class RollingMetrics
6
- attr_reader :number_of_buckets
7
- attr_reader :bucket_size_in_seconds
8
- attr_reader :buckets
9
-
10
- def initialize(options = {})
11
- @number_of_buckets = options.fetch(:number_of_buckets, 6)
12
- @bucket_size_in_seconds = options.fetch(:bucket_size_in_seconds, 10)
13
- reset
14
- end
15
-
16
- def mark_success
17
- timestamp = Time.now.to_i
18
- bucket(timestamp).mark_success
19
- prune_buckets(timestamp)
20
- nil
21
- end
22
-
23
- def mark_failure
24
- timestamp = Time.now.to_i
25
- bucket(timestamp).mark_failure
26
- prune_buckets(timestamp)
27
- nil
28
- end
29
-
30
- def successes
31
- prune_buckets
32
- @buckets.inject(0) { |sum, bucket| sum += bucket.successes }
33
- end
34
-
35
- def failures
36
- prune_buckets
37
- @buckets.inject(0) { |sum, bucket| sum += bucket.failures }
38
- end
39
-
40
- def requests
41
- prune_buckets
42
- @buckets.inject(0) { |sum, bucket| sum += bucket.requests }
43
- end
44
-
45
- def error_percentage
46
- return 0 if failures == 0 || requests == 0
47
- ((failures / requests.to_f) * 100).to_i
48
- end
49
-
50
- def reset
51
- @buckets = []
52
- nil
53
- end
54
-
55
- private
56
-
57
- def bucket(timestamp)
58
- bucket = @buckets.detect { |bucket| bucket.include?(timestamp) }
59
- return bucket if bucket
60
-
61
- bucket = Bucket.new(timestamp, timestamp + @bucket_size_in_seconds - 1)
62
- @buckets.push bucket
63
-
64
- bucket
65
- end
66
-
67
- def prune_buckets(timestamp = Time.now.to_i)
68
- cutoff = timestamp - (@number_of_buckets * @bucket_size_in_seconds)
69
- @buckets.delete_if { |bucket| bucket.prune?(cutoff) }
70
- end
71
- end
72
- end
73
- end
@@ -1,37 +0,0 @@
1
- module Resilient
2
- class CircuitBreaker
3
- class RollingMetrics
4
- class Bucket
5
- attr_reader :successes
6
- attr_reader :failures
7
-
8
- def initialize(timestamp_start, timestamp_end)
9
- @timestamp_start = timestamp_start
10
- @timestamp_end = timestamp_end
11
- @successes = 0
12
- @failures = 0
13
- end
14
-
15
- def mark_success
16
- @successes += 1
17
- end
18
-
19
- def mark_failure
20
- @failures += 1
21
- end
22
-
23
- def requests
24
- @successes + @failures
25
- end
26
-
27
- def include?(timestamp)
28
- timestamp >= @timestamp_start && timestamp <= @timestamp_end
29
- end
30
-
31
- def prune?(timestamp)
32
- @timestamp_end <= timestamp
33
- end
34
- end
35
- end
36
- end
37
- end