resilient 0.4.0 → 0.5.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: 099b9fa420d5b51a3b258c584b730fe03830a882
4
- data.tar.gz: 1e977d958f1b55fe8e96ca833f08961ff19c1ad4
3
+ metadata.gz: a7441e56169aaa25eeeb4e40a73901f939e3af2d
4
+ data.tar.gz: 04f4f827c3fc3de69439b40590ab1c3615e43376
5
5
  SHA512:
6
- metadata.gz: 227075bb296e259d107a45247ca95b84364717e6d341d1df5223e3f4729f9ee629835f776777d42547c2cf5a90e8d8ffb401eebd79c8b527992cc9e85baeeee4
7
- data.tar.gz: 7ffa39266ff521fa573e5196c1a576afd2d530f1ac11eb51522290f2300eddd0d704a6511cf75c90eca14bebd2b68bfe68b5b5f774cd55a2e96aa63df05d155d
6
+ metadata.gz: e54a1abc95f5ba114b2eb4064a2d8f0190014268c7061a6869bc7811278f075331ec13cfa5758738be1690e427cfc38ca768d5f34f2258007eece41b9fc707e6
7
+ data.tar.gz: bf752309fd26f431ae9c04b7dffeb68827bb05835afee1b2beed0548ecbd70333ce53c7983e2855bb2c5ae2bcdfd1f1ce1b70849c03c609f67fe061e31a9cf77
@@ -4,4 +4,5 @@ rvm:
4
4
  - 2.1.4
5
5
  - 2.2.3
6
6
  before_install: gem install bundler -v 1.10.6
7
+ bundler_args: --without=development
7
8
  script: script/test
@@ -0,0 +1,8 @@
1
+ ## 0.5.0
2
+
3
+ * [tweaked interface for metrics](https://github.com/jnunemaker/resilient/pull/15)
4
+ * [stop rounding error percentage](https://github.com/jnunemaker/resilient/commit/6cdade5c3fc5f75a6ffde87082771608d582026c)
5
+
6
+ ## prior to 0.5.0
7
+
8
+ * No changelog kept so just checkout the commits. There aren't too terrible many.
data/Gemfile CHANGED
@@ -1,5 +1,10 @@
1
1
  source "https://rubygems.org"
2
2
  gemspec
3
3
 
4
- gem "guard", "~> 2.13.0"
5
- gem "guard-minitest", "~> 2.4.4"
4
+ gem "timecop", "~> 0.8"
5
+ gem "minitest", "~> 5.8"
6
+
7
+ group :development do
8
+ gem "guard", "~> 2.13.0"
9
+ gem "guard-minitest", "~> 2.4.4"
10
+ end
data/README.md CHANGED
@@ -82,6 +82,20 @@ circuit_breaker = Resilient::CircuitBreaker.get("example", {
82
82
  # etc etc etc
83
83
  ```
84
84
 
85
+ ## Default Properties
86
+
87
+ Property | Default | Notes
88
+ --------------------------------|------------------------|--------
89
+ **:force_open** | false | allows forcing the circuit open (stopping all requests)
90
+ **:force_closed** | false | allows ignoring errors and therefore never trip "open" (ie. allow all traffic through); normal instrumentation will still happen, thus allowing you to "test" configuration live without impact
91
+ **:instrumenter** | Instrumenters::Noop | what to use to instrument all events that happen (ie: ActiveSupport::Notifications)
92
+ **:sleep_window_seconds** | 5 | seconds after tripping circuit before allowing retry
93
+ **:request_volume_threshold** | 20 | number of requests that must be made within a statistical window before open/close decisions are made using stats
94
+ **:error_threshold_percentage** | 50 | % of "marks" that must be failed to trip the circuit
95
+ **:window_size_in_seconds** | 60 | number of seconds in the statistical window
96
+ **:bucket_size_in_seconds** | 10 | size of buckets in statistical window
97
+ **:metrics** | Resilient::Metrics.new | metrics instance used to keep track of success and failure
98
+
85
99
  ## Tests
86
100
 
87
101
  To ensure that you have clean circuit breakers for each test case, be sure to run the following in the setup for your tests (which resets the default registry and thus clears all the registered circuits) either before every test case or at a minimum each test case that uses circuit breakers.
@@ -0,0 +1,114 @@
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 "minitest/autorun"
10
+ require "resilient/circuit_breaker"
11
+ require "resilient/test/metrics_interface"
12
+
13
+ # Metrics class that closes circuit on every success call and opens circuit for
14
+ # sleep_window_seconds on every failure.
15
+ class BinaryMetrics
16
+ def initialize(options = {})
17
+ reset
18
+ end
19
+
20
+ def success
21
+ reset
22
+ nil
23
+ end
24
+
25
+ def failure
26
+ @closed = false
27
+ nil
28
+ end
29
+
30
+ def reset
31
+ @closed = true
32
+ nil
33
+ end
34
+
35
+ def under_request_volume_threshold?(request_volume_threshold)
36
+ false
37
+ end
38
+
39
+ def under_error_threshold_percentage?(error_threshold_percentage)
40
+ @closed
41
+ end
42
+ end
43
+
44
+ class BinaryMetricsTest < Minitest::Test
45
+ def setup
46
+ @object = BinaryMetrics.new
47
+ end
48
+
49
+ include Resilient::Test::MetricsInterface
50
+ end
51
+
52
+ circuit_breaker = Resilient::CircuitBreaker.get("example", {
53
+ sleep_window_seconds: 1,
54
+ metrics: BinaryMetrics.new,
55
+ })
56
+
57
+ # success
58
+ if circuit_breaker.allow_request?
59
+ begin
60
+ puts "do expensive thing"
61
+ circuit_breaker.success
62
+ rescue => boom
63
+ # won't get here in this example
64
+ circuit_breaker.failure
65
+ end
66
+ else
67
+ raise "will not get here"
68
+ end
69
+
70
+ # failure
71
+ if circuit_breaker.allow_request?
72
+ begin
73
+ raise
74
+ rescue => boom
75
+ circuit_breaker.failure
76
+ puts "failed slow, do fallback"
77
+ end
78
+ else
79
+ raise "will not get here"
80
+ end
81
+
82
+ # fail fast
83
+ if circuit_breaker.allow_request?
84
+ raise "will not get here"
85
+ else
86
+ puts "failed fast, do fallback"
87
+ end
88
+
89
+ start = Time.now
90
+
91
+ while (Time.now - start) < 3
92
+ if circuit_breaker.allow_request?
93
+ puts "doing a single attempt as we've failed fast for sleep_window_seconds"
94
+ break
95
+ else
96
+ puts "failed fast, do fallback"
97
+ end
98
+ sleep rand(0.1)
99
+ end
100
+
101
+ if circuit_breaker.allow_request?
102
+ raise "will not get here"
103
+ else
104
+ puts "request denied because single request has not been marked success yet"
105
+ end
106
+
107
+ puts "marking single request as success"
108
+ circuit_breaker.success
109
+
110
+ if circuit_breaker.allow_request?
111
+ puts "circuit reset and back closed now, allowing requests"
112
+ else
113
+ raise "will not get here"
114
+ end
@@ -117,11 +117,11 @@ module Resilient
117
117
  end
118
118
 
119
119
  def under_request_volume_threshold?
120
- metrics.requests < @properties.request_volume_threshold
120
+ metrics.under_request_volume_threshold?(@properties.request_volume_threshold)
121
121
  end
122
122
 
123
123
  def under_error_threshold_percentage?
124
- metrics.error_percentage < @properties.error_threshold_percentage
124
+ metrics.under_error_threshold_percentage?(@properties.error_threshold_percentage)
125
125
  end
126
126
 
127
127
  def open?
@@ -30,6 +30,14 @@ module Resilient
30
30
  @buckets = []
31
31
  end
32
32
 
33
+ def under_request_volume_threshold?(request_volume_threshold)
34
+ requests < request_volume_threshold
35
+ end
36
+
37
+ def under_error_threshold_percentage?(error_threshold_percentage)
38
+ error_percentage < error_threshold_percentage
39
+ end
40
+
33
41
  def success
34
42
  @storage.increment(current_bucket, StorageSuccessKeys)
35
43
  prune_buckets
@@ -42,15 +50,12 @@ module Resilient
42
50
  nil
43
51
  end
44
52
 
45
- def successes
46
- prune_buckets
47
- @storage.sum(@buckets, :successes)[:successes]
53
+ def reset
54
+ @storage.prune(@buckets, StorageKeys)
55
+ nil
48
56
  end
49
57
 
50
- def failures
51
- prune_buckets
52
- @storage.sum(@buckets, :failures)[:failures]
53
- end
58
+ private
54
59
 
55
60
  def requests
56
61
  prune_buckets
@@ -71,16 +76,9 @@ module Resilient
71
76
  requests = successes + failures
72
77
  return 0 if failures == 0 || requests == 0
73
78
 
74
- ((failures / requests.to_f) * 100).round
79
+ (failures / requests.to_f) * 100
75
80
  end
76
81
 
77
- def reset
78
- @storage.prune(@buckets, StorageKeys)
79
- nil
80
- end
81
-
82
- private
83
-
84
82
  def current_bucket(timestamp = Time.now.to_i)
85
83
  bucket = @buckets.detect { |bucket| bucket.include?(timestamp) }
86
84
  return bucket if bucket
@@ -1,6 +1,16 @@
1
1
  module Resilient
2
2
  class Test
3
3
  module MetricsInterface
4
+ def test_responds_to_under_request_volume_threshold_predicate
5
+ assert_respond_to @object, :under_request_volume_threshold?
6
+ assert_equal 1, @object.method(:under_request_volume_threshold?).arity
7
+ end
8
+
9
+ def test_responds_to_under_error_threshold_percentage_predicate
10
+ assert_respond_to @object, :under_error_threshold_percentage?
11
+ assert_equal 1, @object.method(:under_error_threshold_percentage?).arity
12
+ end
13
+
4
14
  def test_responds_to_success
5
15
  assert_respond_to @object, :success
6
16
  end
@@ -17,22 +27,6 @@ module Resilient
17
27
  assert_nil @object.failure
18
28
  end
19
29
 
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
30
  def test_responds_to_reset
37
31
  assert_respond_to @object, :reset
38
32
  end
@@ -1,3 +1,3 @@
1
1
  module Resilient
2
- VERSION = "0.4.0"
2
+ VERSION = "0.5.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.4.0
4
+ version: 0.5.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: 2016-04-15 00:00:00.000000000 Z
11
+ date: 2016-07-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -61,11 +61,13 @@ extra_rdoc_files: []
61
61
  files:
62
62
  - ".gitignore"
63
63
  - ".travis.yml"
64
+ - CHANGELOG.md
64
65
  - Gemfile
65
66
  - Guardfile
66
67
  - LICENSE.txt
67
68
  - README.md
68
69
  - examples/basic.rb
70
+ - examples/binary.rb
69
71
  - examples/get_vs_new.rb
70
72
  - examples/long_running.rb
71
73
  - lib/resilient.rb