degrade 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
@@ -0,0 +1,21 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 James Golick
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,84 @@
1
+ = degrade
2
+
3
+ Keep track of error rates using redis. Degrade functionality if they're too high.
4
+
5
+ == Install it
6
+
7
+ gem install degrade
8
+
9
+ == How it works
10
+
11
+ Setup one instance per feature.
12
+
13
+ $redis = Redis.new
14
+ $rollout = Rollout.new($redis) # see http://github.com/jamesgolick/rollout
15
+ $degrade_cassandra = Degrade.new(@redis, :name => :cassandra,
16
+ :sample => 5000, # optional - 5000 is the default
17
+ :minimum => 100, # optional - 100 is the default
18
+ :threshold => 0.1, # optional - 0.1 is the default
19
+ :errors => [StandardError], # optional - [StandardError] is the default
20
+ :failure_strategy => lambda { $rollout.deactivate_all(:cassandra) }
21
+
22
+ There are a bunch of options here:
23
+
24
+ * name: The name of the feature.
25
+ * sample: We zero the counters every n requests. Define n here.
26
+ * minimum: What's the minimum number of requests we need to see before we decide that the threshold has been met? Without this, if the first request of a given sample is a failure, we'd be failing the service.
27
+ * threshold: Percentage of failure requests necessary to trigger a failure.
28
+ * errors: Which errors should cause us to mark a failures?
29
+ * failure_strategy: The proc that will get called when the threshold is met. See http://github.com/jamesgolick/rollout if you don't have an existing system for disabling features on the fly.
30
+
31
+ The only thing left to do is wrap calls to whatever it is that might fail in a call to perform():
32
+
33
+ $degrade_cassandra.perform { $cassandra.get("Timelines", "1/everything") }
34
+
35
+ Note that degrade doesn't actually handle the degradation for you. You still have to wrap sections of functionality with whatever mechanism you use to disable features (i.e. rollout).
36
+
37
+ == Driver decorators
38
+
39
+ Wrapping every call to a service in $degrade.perform() is a hassle and will require a lot of changes to your code. You're better off decorating your driver.
40
+
41
+ Let's say we had a service called NewsService:
42
+
43
+ class NewsService < SomeRPCMechanism
44
+ def get_news(*args)
45
+ # ...
46
+ end
47
+ end
48
+
49
+ We'd decorate it like this:
50
+
51
+ class NewsServiceWithDegradation
52
+ def initialize(news_service, degrade)
53
+ @news_service = news_service
54
+ @degrade = degrade
55
+ end
56
+
57
+ def get_news(*args)
58
+ @degrade.perform { @news_service.get_news(*args) }
59
+ end
60
+ end
61
+
62
+ We're doing this for one of our services and it's working pretty well.
63
+
64
+ == Known issues / concerns
65
+
66
+ Making a bunch of requests to services whose drivers are wrapped in this could cause problems if your services / site are extremely high throughput. This could be improved somewhat by adding some randomness to threshold checking and sample resetting so that those things aren't checked every request. The service we're currently using this with runs at about 25 req / s which is a _long_ way before any problems are likely.
67
+
68
+ Also, obviously calls to redis add latency to your requests. Make sure you're okay with this.
69
+
70
+ Don't try to use this on redis itself. If you do, the universe will enter a state of infinite recursion.
71
+
72
+ == Note on Patches/Pull Requests
73
+
74
+ * Fork the project.
75
+ * Make your feature addition or bug fix.
76
+ * Add tests for it. This is important so I don't break it in a
77
+ future version unintentionally.
78
+ * Commit, do not mess with rakefile, version, or history.
79
+ (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
80
+ * Send me a pull request. Bonus points for topic branches.
81
+
82
+ == Copyright
83
+
84
+ Copyright (c) 2010 James Golick. See LICENSE for details.
@@ -0,0 +1,47 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "degrade"
8
+ gem.summary = %Q{Keep track of error rates using redis. Degrade functionality if they're too high.}
9
+ gem.description = %Q{Keep track of error rates using redis. Degrade functionality if they're too high.}
10
+ gem.email = "jamesgolick@gmail.com"
11
+ gem.homepage = "http://github.com/jamesgolick/degrade"
12
+ gem.authors = ["James Golick"]
13
+ gem.add_development_dependency "rspec", "1.2.9"
14
+ gem.add_development_dependency "bourne", "1.0.0"
15
+ gem.add_development_dependency "redis", "0.1.0"
16
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
17
+ end
18
+ Jeweler::GemcutterTasks.new
19
+ rescue LoadError
20
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
21
+ end
22
+
23
+ require 'spec/rake/spectask'
24
+ Spec::Rake::SpecTask.new(:spec) do |spec|
25
+ spec.libs << 'lib' << 'spec'
26
+ spec.spec_files = FileList['spec/**/*_spec.rb']
27
+ end
28
+
29
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
30
+ spec.libs << 'lib' << 'spec'
31
+ spec.pattern = 'spec/**/*_spec.rb'
32
+ spec.rcov = true
33
+ end
34
+
35
+ task :spec => :check_dependencies
36
+
37
+ task :default => :spec
38
+
39
+ require 'rake/rdoctask'
40
+ Rake::RDocTask.new do |rdoc|
41
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
42
+
43
+ rdoc.rdoc_dir = 'rdoc'
44
+ rdoc.title = "degrade #{version}"
45
+ rdoc.rdoc_files.include('README*')
46
+ rdoc.rdoc_files.include('lib/**/*.rb')
47
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,62 @@
1
+ class Degrade
2
+ def initialize(redis, options)
3
+ @redis = redis
4
+ @name = options[:name]
5
+ @minimum = options[:minimum] || 100
6
+ @sample = options[:sample] || 5000
7
+ @threshold = options[:threshold] || 0.1
8
+ @errors = options[:errors] || [StandardError]
9
+ @failure_strategy = options[:failure_strategy]
10
+ end
11
+
12
+ def perform
13
+ begin
14
+ mark_request
15
+ yield
16
+ rescue *@errors => e
17
+ mark_failure
18
+ raise e
19
+ end
20
+ end
21
+
22
+ def requests
23
+ @redis.get(requests_key).to_f
24
+ end
25
+
26
+ def failures
27
+ @redis.get(failures_key).to_f
28
+ end
29
+
30
+ private
31
+ def requests_key
32
+ "status:#{@name}:requests"
33
+ end
34
+
35
+ def failures_key
36
+ "status:#{@name}:failures"
37
+ end
38
+
39
+ def mark_request
40
+ @redis.incr(requests_key)
41
+ reset_sample
42
+ end
43
+
44
+ def mark_failure
45
+ @redis.incr(failures_key)
46
+ check_threshold
47
+ end
48
+
49
+ def check_threshold
50
+ total_requests = requests
51
+ return if total_requests < @minimum
52
+
53
+ @failure_strategy.call if failures / total_requests >= @threshold
54
+ end
55
+
56
+ def reset_sample
57
+ if requests > @sample
58
+ @redis.del(requests_key)
59
+ @redis.del(failures_key)
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,57 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe "Degrade" do
4
+ before do
5
+ @redis = Redis.new
6
+ @failure_proc = stub(:call => nil)
7
+ @degrade = Degrade.new(@redis, :name => :chat_service,
8
+ :sample => 100,
9
+ :minimum => 100,
10
+ :threshold => 0.1,
11
+ :errors => [StandardError],
12
+ :failure_strategy => @failure_proc)
13
+ end
14
+
15
+ describe "when there's an error" do
16
+ it "reraises the error" do
17
+ lambda { @degrade.perform { raise "asdf" } }.should raise_error
18
+ end
19
+ end
20
+
21
+ describe "when the threshold is reached before the minimum number of requests" do
22
+ before do
23
+ 9.times { @degrade.perform { } }
24
+ @degrade.perform { raise "" } rescue nil
25
+ end
26
+
27
+ it "doesn't call the failure strategy" do
28
+ @failure_proc.should_not have_received(:call)
29
+ end
30
+ end
31
+
32
+ describe "when the threshold is reached" do
33
+ before do
34
+ 90.times { @degrade.perform {} }
35
+ 10.times { @degrade.perform { raise "" } rescue nil }
36
+ end
37
+
38
+ it "calls the failure strategy" do
39
+ @failure_proc.should have_received(:call)
40
+ end
41
+ end
42
+
43
+ describe "when the requests grow above the sample" do
44
+ before do
45
+ @degrade.perform { raise "" } rescue nil
46
+ 100.times { @degrade.perform { } }
47
+ end
48
+
49
+ it "resets the counter" do
50
+ @degrade.requests.should == 0
51
+ end
52
+
53
+ it "resets the requests" do
54
+ @degrade.failures.should == 0
55
+ end
56
+ end
57
+ end
@@ -0,0 +1 @@
1
+ --color
@@ -0,0 +1,12 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
2
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
3
+ require 'degrade'
4
+ require 'spec'
5
+ require 'spec/autorun'
6
+ require 'bourne'
7
+ require 'redis'
8
+
9
+ Spec::Runner.configure do |config|
10
+ config.mock_with :mocha
11
+ config.before { Redis.new.flushdb }
12
+ end
metadata ADDED
@@ -0,0 +1,125 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: degrade
3
+ version: !ruby/object:Gem::Version
4
+ hash: 27
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - 0
10
+ version: 0.1.0
11
+ platform: ruby
12
+ authors:
13
+ - James Golick
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2010-07-17 00:00:00 -07:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: rspec
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - "="
28
+ - !ruby/object:Gem::Version
29
+ hash: 13
30
+ segments:
31
+ - 1
32
+ - 2
33
+ - 9
34
+ version: 1.2.9
35
+ type: :development
36
+ version_requirements: *id001
37
+ - !ruby/object:Gem::Dependency
38
+ name: bourne
39
+ prerelease: false
40
+ requirement: &id002 !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - "="
44
+ - !ruby/object:Gem::Version
45
+ hash: 23
46
+ segments:
47
+ - 1
48
+ - 0
49
+ - 0
50
+ version: 1.0.0
51
+ type: :development
52
+ version_requirements: *id002
53
+ - !ruby/object:Gem::Dependency
54
+ name: redis
55
+ prerelease: false
56
+ requirement: &id003 !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - "="
60
+ - !ruby/object:Gem::Version
61
+ hash: 27
62
+ segments:
63
+ - 0
64
+ - 1
65
+ - 0
66
+ version: 0.1.0
67
+ type: :development
68
+ version_requirements: *id003
69
+ description: Keep track of error rates using redis. Degrade functionality if they're too high.
70
+ email: jamesgolick@gmail.com
71
+ executables: []
72
+
73
+ extensions: []
74
+
75
+ extra_rdoc_files:
76
+ - LICENSE
77
+ - README.rdoc
78
+ files:
79
+ - .document
80
+ - .gitignore
81
+ - LICENSE
82
+ - README.rdoc
83
+ - Rakefile
84
+ - VERSION
85
+ - lib/degrade.rb
86
+ - spec/degrade_spec.rb
87
+ - spec/spec.opts
88
+ - spec/spec_helper.rb
89
+ has_rdoc: true
90
+ homepage: http://github.com/jamesgolick/degrade
91
+ licenses: []
92
+
93
+ post_install_message:
94
+ rdoc_options:
95
+ - --charset=UTF-8
96
+ require_paths:
97
+ - lib
98
+ required_ruby_version: !ruby/object:Gem::Requirement
99
+ none: false
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ hash: 3
104
+ segments:
105
+ - 0
106
+ version: "0"
107
+ required_rubygems_version: !ruby/object:Gem::Requirement
108
+ none: false
109
+ requirements:
110
+ - - ">="
111
+ - !ruby/object:Gem::Version
112
+ hash: 3
113
+ segments:
114
+ - 0
115
+ version: "0"
116
+ requirements: []
117
+
118
+ rubyforge_project:
119
+ rubygems_version: 1.3.7
120
+ signing_key:
121
+ specification_version: 3
122
+ summary: Keep track of error rates using redis. Degrade functionality if they're too high.
123
+ test_files:
124
+ - spec/degrade_spec.rb
125
+ - spec/spec_helper.rb