resilient 0.0.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 822421a58f373992d96937a055b287a2f633ef1b
4
+ data.tar.gz: 6e29f01491cce3b1244998c9dc38758db56bc58d
5
+ SHA512:
6
+ metadata.gz: d6952a74ffed454c5c1909991db5dfd056dfb5f26932bc1537776cee363dcc55cbeff6b549da1ac112f124437032b92d24c071e516a9702bc72bd747187a3522
7
+ data.tar.gz: 9d1edae9fb8f13856fc988af0b257a7d2d2e184a04068e5dc1f054d945666c49cbe934ee5ff23e8a983ed074b27f527f5258bdcfced828ef420434bfd5669d8b
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.1.4
4
+ before_install: gem install bundler -v 1.10.6
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in resilient.gemspec
4
+ gemspec
5
+
6
+ gem "guard", "~> 2.13.0"
7
+ gem "guard-minitest", "~> 2.4.4"
8
+ gem "timecop", "0.8.0"
data/Guardfile ADDED
@@ -0,0 +1,23 @@
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
+ guard :minitest do
19
+ watch(%r{^test/(.*)\/?(.*)_test\.rb$}) { "test" }
20
+ watch(%r{^lib/(.*/)?([^/]+)\.rb$}) { "test" }
21
+ watch(%r{^test/test_helper\.rb$}) { "test" }
22
+ watch(%r{test/support/.*}) { "test" }
23
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 John Nunemaker
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,105 @@
1
+ # Resilient
2
+
3
+ Some tools for resilient in ruby.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem "resilient"
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install resilient
20
+
21
+ ## Usage
22
+
23
+ ```ruby
24
+ require "resilient/circuit_breaker"
25
+
26
+ # default config for circuit
27
+ circuit_breaker = Resilient::CircuitBreaker.new
28
+ if circuit_breaker.request_allowed?
29
+ begin
30
+ # do something expensive
31
+ circuit_breaker.mark_success
32
+ rescue => boom
33
+ # do fallback
34
+ circuit_breaker.mark_failure
35
+ end
36
+ else
37
+ # do fallback
38
+ end
39
+ ```
40
+
41
+ customize config of circuit:
42
+
43
+ ```ruby
44
+ config = Resilient::CircuitBreaker::RollingConfig.new({
45
+ # at what percentage of errors should we open the circuit
46
+ error_threshold_percentage: 50,
47
+ # do not try request again for 5 seconds
48
+ sleep_window_seconds: 5,
49
+ # do not open circuit until at least 5 requests have happened
50
+ request_volume_threshold: 5,
51
+ })
52
+ circuit_breaker = Resilient::CircuitBreaker.new(config: config)
53
+ # etc etc etc
54
+ ```
55
+
56
+ force the circuit to be always open:
57
+
58
+ ```ruby
59
+ config = Resilient::CircuitBreaker::RollingConfig.new(force_open: true)
60
+ circuit_breaker = Resilient::CircuitBreaker.new(config: config)
61
+ # etc etc etc
62
+ ```
63
+
64
+ force the circuit to be always closed:
65
+
66
+ ```ruby
67
+ config = Resilient::CircuitBreaker::RollingConfig.new(force_closed: true)
68
+ circuit_breaker = Resilient::CircuitBreaker.new(config: config)
69
+ # etc etc etc
70
+ ```
71
+
72
+ customize rolling window to be 10 buckets of 1 second each (10 seconds in all):
73
+
74
+ ```ruby
75
+ metrics = Resilient::CircuitBreaker::RollingMetrics.new({
76
+ number_of_buckets: 10,
77
+ bucket_size_in_seconds: 1,
78
+ })
79
+ circuit_breaker = Resilient::CircuitBreaker.new(metrics: metrics)
80
+ # etc etc etc
81
+ ```
82
+
83
+ ## Development
84
+
85
+ ```bash
86
+ # install dependencies
87
+ script/bootstrap
88
+
89
+ # run tests
90
+ script/test
91
+
92
+ # ...or to auto run tests with guard
93
+ script/watch
94
+
95
+ # to get a shell to play in
96
+ script/console
97
+ ```
98
+
99
+ ## Contributing
100
+
101
+ Bug reports and pull requests are welcome on GitHub at https://github.com/jnunemaker/resilient.
102
+
103
+ ## License
104
+
105
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,10 @@
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
data/TODO.md ADDED
@@ -0,0 +1,4 @@
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?
@@ -0,0 +1,23 @@
1
+ module Resilient
2
+ class CircuitBreaker
3
+ class RollingConfig
4
+ attr_reader :error_threshold_percentage
5
+ attr_reader :sleep_window_seconds
6
+ attr_reader :request_volume_threshold
7
+ attr_reader :force_open
8
+ attr_reader :force_closed
9
+ attr_reader :number_of_buckets
10
+ attr_reader :bucket_size_in_seconds
11
+
12
+ def initialize(options = {})
13
+ @force_open = options.fetch(:force_open, false)
14
+ @force_closed = options.fetch(:force_closed, false)
15
+ @sleep_window_seconds = options.fetch(:sleep_window_seconds, 5)
16
+ @request_volume_threshold = options.fetch(:request_volume_threshold, 20)
17
+ @error_threshold_percentage = options.fetch(:error_threshold_percentage, 50)
18
+ @number_of_buckets = options.fetch(:number_of_buckets, 6)
19
+ @bucket_size_in_seconds = options.fetch(:bucket_size_in_seconds, 10)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,37 @@
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
@@ -0,0 +1,73 @@
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
@@ -0,0 +1,92 @@
1
+ require "resilient/circuit_breaker/rolling_metrics"
2
+ require "resilient/circuit_breaker/rolling_config"
3
+
4
+ module Resilient
5
+ class CircuitBreaker
6
+ attr_reader :metrics
7
+ attr_reader :config
8
+ attr_reader :open
9
+ attr_reader :opened_or_last_checked_at_epoch
10
+
11
+ def initialize(open: false, config: RollingConfig.new, metrics: RollingMetrics.new)
12
+ @open = open
13
+ @opened_or_last_checked_at_epoch = 0
14
+ @config = config
15
+ @metrics = if metrics
16
+ metrics
17
+ else
18
+ RollingMetrics.new({
19
+ number_of_buckets: config.number_of_buckets,
20
+ bucket_size_in_seconds: config.bucket_size_in_seconds,
21
+ })
22
+ end
23
+ end
24
+
25
+ def allow_request?
26
+ return false if @config.force_open
27
+ return true if @config.force_closed
28
+
29
+ closed? || allow_single_request?
30
+ end
31
+
32
+ def mark_success
33
+ close_circuit if @open
34
+ end
35
+
36
+ def mark_failure
37
+ @metrics.mark_failure
38
+ end
39
+
40
+ def reset
41
+ @open = false
42
+ @opened_or_last_checked_at_epoch = 0
43
+ @metrics.reset
44
+ nil
45
+ end
46
+
47
+ private
48
+
49
+ def open_circuit
50
+ @open = true
51
+ @opened_or_last_checked_at_epoch = Time.now.to_i
52
+ end
53
+
54
+ def close_circuit
55
+ @open = false
56
+ @opened_or_last_checked_at_epoch = 0
57
+ @metrics.reset
58
+ end
59
+
60
+ def under_request_volume_threshold?
61
+ @metrics.requests < @config.request_volume_threshold
62
+ end
63
+
64
+ def under_error_threshold_percentage?
65
+ @metrics.error_percentage < @config.error_threshold_percentage
66
+ end
67
+
68
+ def open?
69
+ return true if @open
70
+ return false if under_request_volume_threshold?
71
+ return false if under_error_threshold_percentage?
72
+
73
+ open_circuit
74
+ end
75
+
76
+ def closed?
77
+ !open?
78
+ end
79
+
80
+ def allow_single_request?
81
+ try_next_request_at = @opened_or_last_checked_at_epoch + @config.sleep_window_seconds
82
+ now = Time.now.to_i
83
+
84
+ if @open && now > try_next_request_at
85
+ @opened_or_last_checked_at_epoch = now + @config.sleep_window_seconds
86
+ true
87
+ else
88
+ false
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,3 @@
1
+ module Resilient
2
+ VERSION = "0.0.1"
3
+ end
data/lib/resilient.rb ADDED
@@ -0,0 +1,4 @@
1
+ require "resilient/version"
2
+
3
+ module Resilient
4
+ end
data/resilient.gemspec ADDED
@@ -0,0 +1,24 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'resilient/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "resilient"
8
+ spec.version = Resilient::VERSION
9
+ spec.authors = ["John Nunemaker"]
10
+ spec.email = ["nunemaker@gmail.com"]
11
+
12
+ spec.summary = %q{toolkit for resilient ruby apps}
13
+ spec.homepage = "https://github.com/jnunemaker/resilient"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17
+ spec.bindir = "exe"
18
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.10"
22
+ spec.add_development_dependency "rake", "~> 10.0"
23
+ spec.add_development_dependency "minitest", "~> 5.8"
24
+ end
data/script/bootstrap ADDED
@@ -0,0 +1 @@
1
+ bundle --quiet
data/script/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "resilient"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
data/script/release ADDED
@@ -0,0 +1,42 @@
1
+ #!/bin/sh
2
+ #/ Usage: release
3
+ #/
4
+ #/ Tag the version in the repo and push 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
+
15
+ gem_name=resilient
16
+
17
+ # Build a new gem archive.
18
+ rm -rf $gem_name-*.gem
19
+ gem build -q $gem_name.gemspec
20
+
21
+ # Make sure we're on the master branch.
22
+ (git branch | grep -q '* master') || {
23
+ echo "Only release from the master branch."
24
+ exit 1
25
+ }
26
+
27
+ # Figure out what version we're releasing.
28
+ tag=v`ls $gem_name-*.gem | sed "s/^$gem_name-\(.*\)\.gem$/\1/"`
29
+
30
+ echo "Releasing $tag"
31
+
32
+ # Make sure we haven't released this version before.
33
+ git fetch -t origin
34
+
35
+ (git tag -l | grep -q "$tag") && {
36
+ echo "Whoops, there's already a '${tag}' tag."
37
+ exit 1
38
+ }
39
+
40
+ # Tag it and bag it.
41
+ gem push $gem_name-*.gem && git tag "$tag" &&
42
+ git push origin master && git push origin "$tag"
data/script/test ADDED
@@ -0,0 +1,25 @@
1
+ #!/bin/sh
2
+ #/ Usage: test [individual test file]
3
+ #/
4
+ #/ Bootstrap and run all tests or an individual test.
5
+ #/
6
+ #/ Examples:
7
+ #/
8
+ #/ # run all tests
9
+ #/ test
10
+ #/
11
+ #/ # run individual test
12
+ #/ test test/controller_instrumentation_test.rb
13
+ #/
14
+
15
+ set -e
16
+ cd $(dirname "$0")/..
17
+
18
+ [ "$1" = "--help" -o "$1" = "-h" -o "$1" = "help" ] && {
19
+ grep '^#/' <"$0"| cut -c4-
20
+ exit 0
21
+ }
22
+
23
+ ruby -I lib -I test -r rubygems \
24
+ -e 'require "bundler/setup"' \
25
+ -e '(ARGV.empty? ? Dir["test/**/*_test.rb"] : ARGV).each { |f| load f }' -- "$@"
data/script/watch ADDED
@@ -0,0 +1 @@
1
+ bundle exec guard
metadata ADDED
@@ -0,0 +1,106 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: resilient
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - John Nunemaker
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2015-12-07 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.10'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.10'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: minitest
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '5.8'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '5.8'
55
+ description:
56
+ email:
57
+ - nunemaker@gmail.com
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - ".gitignore"
63
+ - ".travis.yml"
64
+ - Gemfile
65
+ - Guardfile
66
+ - LICENSE.txt
67
+ - README.md
68
+ - Rakefile
69
+ - TODO.md
70
+ - lib/resilient.rb
71
+ - 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
75
+ - lib/resilient/version.rb
76
+ - resilient.gemspec
77
+ - script/bootstrap
78
+ - script/console
79
+ - script/release
80
+ - script/test
81
+ - script/watch
82
+ homepage: https://github.com/jnunemaker/resilient
83
+ licenses:
84
+ - MIT
85
+ metadata: {}
86
+ post_install_message:
87
+ rdoc_options: []
88
+ require_paths:
89
+ - lib
90
+ required_ruby_version: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
95
+ required_rubygems_version: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - ">="
98
+ - !ruby/object:Gem::Version
99
+ version: '0'
100
+ requirements: []
101
+ rubyforge_project:
102
+ rubygems_version: 2.2.2
103
+ signing_key:
104
+ specification_version: 4
105
+ summary: toolkit for resilient ruby apps
106
+ test_files: []