degrade 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +21 -0
- data/LICENSE +20 -0
- data/README.rdoc +84 -0
- data/Rakefile +47 -0
- data/VERSION +1 -0
- data/lib/degrade.rb +62 -0
- data/spec/degrade_spec.rb +57 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +12 -0
- metadata +125 -0
data/.document
ADDED
data/.gitignore
ADDED
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.
|
data/README.rdoc
ADDED
@@ -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.
|
data/Rakefile
ADDED
@@ -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
|
data/lib/degrade.rb
ADDED
@@ -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
|
data/spec/spec.opts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/spec/spec_helper.rb
ADDED
@@ -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
|