resilient 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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: []