success_tracker 0.5

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in success_tracker.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 autohaus24 GmbH
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,88 @@
1
+ # SuccessTracker
2
+
3
+ Allows you to track success and failure of tasks and define thresholds for unexpected failure conditions. When the threshold is met, the failure method returns true (otherwise false). This can be used to define in code what to do when the failure condition is met. The failure method gets the name of a rule as a second parameter. This rule is used to define when a failure should be seen as unexpected. There are some rules defined as default (:percent\_10 which fails when there is at least a 10 percent failure rate with a minimum of 10 records and :sequence\_of\_5 which fails when there are at least 5 failures in a row).
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'success_tracker'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install success_tracker
18
+
19
+ ## Usage
20
+
21
+ ```ruby
22
+ $success_tracker = SuccessTracker::Base.new(redis_connection)
23
+ $success_tracker.success('mytask')
24
+ if $success_tracker.failure('mytask', :percent_10)
25
+ puts "reached the threshold!"
26
+ end
27
+ ```
28
+
29
+ You can define additional rules with your own code blocks or use the sequence_rule or ratio_rule class methods for this task. These code blocks get an array of the recorded notifications as parameters which can have between 0 and 100 elements all having either an empty string (failure) or a "1" (success).
30
+
31
+ ```ruby
32
+ $success_tracker = SuccessTracker::Base.new(redis_connection, :rules => {
33
+ :percent_50 => SuccessTracker::Base.ratio_rule(0.5)
34
+ })
35
+ $success_tracker.failure('mytask', :percent_50)
36
+ ```
37
+
38
+ You can give a block to the failure method which is always yielded. In case the block raises an exception and the failure condition is not met, the exception is extended with the SuccessTracker::NonSignificantError module. This can then be used in rescue statements to define what should only happen if the handled exception was not triggered with a failure condition met.
39
+
40
+ ```ruby
41
+ $success_tracker = SuccessTracker::Base.new(redis_connection)
42
+ $success_tracker.failure('mytask', :percent_10) do
43
+ raise ArgumentError
44
+ end
45
+
46
+ # You can also use it to exclude these errors from Airbrake reporting.
47
+ Airbrake.configure do |config|
48
+ config.ignore_by_filter do |notice|
49
+ SuccessTracker::NonSignificantError === notice.exception
50
+ end
51
+ end
52
+ ```
53
+
54
+ By default all StandardErrors are extended but in the options you can define an array with the exceptions which should be tagged.
55
+
56
+ ```ruby
57
+ $success_tracker = SuccessTracker::Base.new(redis_connection)
58
+ $success_tracker.failure('mytask', :percent_10, :exceptions => [ MySpecialError ]) do
59
+ raise ArgumentError # will not be tagged because it is the wrong exception type
60
+ end
61
+ ```
62
+
63
+ You can also define on_success and on_failure callbacks which run on success or failure and get the identifier given to the success or failure method as a parameter. Use this for example to track success and failure rates in StatsD.
64
+
65
+ ```ruby
66
+ # assuming you have statsd-client stored in $statsd
67
+ $success_tracker = SuccessTracker::Base.new(redis_connection, {
68
+ :on_success => lambda { |identifier| $statsd.increment("#{identifier}.success") },
69
+ :on_failure => lambda { |identifier| $statsd.increment("#{identifier}.failure") }
70
+ }
71
+ )
72
+ ```
73
+
74
+ ## Requirements
75
+
76
+ * Redis
77
+
78
+ ## Contributing
79
+
80
+ 1. Fork it
81
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
82
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
83
+ 4. Push to the branch (`git push origin my-new-feature`)
84
+ 5. Create new Pull Request
85
+
86
+ ## License
87
+
88
+ MIT License. Copyright 2013 autohaus24 GmbH
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ require 'rake/testtask'
4
+
5
+ Rake::TestTask.new(:test) do |t|
6
+ t.libs << 'lib'
7
+ t.libs << 'test'
8
+ t.pattern = 'test/**/*_test.rb'
9
+ t.verbose = false
10
+ end
@@ -0,0 +1,75 @@
1
+ require "success_tracker/version"
2
+ require 'redis'
3
+
4
+ module SuccessTracker
5
+ module NonSignificantError
6
+ end
7
+
8
+ class Base
9
+ attr_accessor :callbacks, :rules, :redis, :list_length
10
+
11
+ def initialize(redis, options={})
12
+ @redis = redis
13
+
14
+ @rules = {
15
+ :percent_10 => self.class.ratio_rule(0.1),
16
+ :sequence_of_5 => self.class.sequence_rule(5),
17
+ }.merge(options.delete(:rules) || {})
18
+ @callbacks = options.delete(:callbacks) || {}
19
+ raise ArgumentError unless options.empty?
20
+
21
+ @list_length = 100
22
+ end
23
+
24
+ def prefix(identifier); "success_tracker:#{identifier}" end
25
+
26
+ def success(identifier)
27
+ callbacks[:success].call(identifier) if callbacks[:success]
28
+ store(identifier, "1")
29
+ end
30
+
31
+ # +identifier+ is the key used for grouping success and failure cases together.
32
+ # +notify_rule+ is a symbol identifying the code block which is evaluated to know if the error is significant or not.
33
+ # + options+ is a hash which currently can only contain the list of exceptions which should be tagged with the NonSignificantError module
34
+ # the given block is always evaluated and the resulting errors are tagged with the NonSignificantError and reraised
35
+ def failure(identifier, notify_rule, options={})
36
+ callbacks[:failure].call(identifier) if callbacks[:failure]
37
+ store(identifier, nil)
38
+
39
+ redis.del(prefix(identifier)) if notify = rules[notify_rule].call(redis.lrange(prefix(identifier), 0,-1))
40
+
41
+ begin
42
+ yield if block_given?
43
+ rescue *(options[:exceptions] || [StandardError]) => error
44
+ error.extend(NonSignificantError) unless notify
45
+ raise
46
+ end
47
+
48
+ return notify
49
+ end
50
+
51
+ # yields the given code block and then marks success. In case a exception was triggered it marks a failure and reraises the exception (for the arguments see the #failure method)
52
+ def track(identifier, notify_rule, options={})
53
+ yield.tap { success(identifier) }
54
+ rescue => exception
55
+ failure(identifier, notify_rule, options) { raise exception }
56
+ end
57
+
58
+
59
+ # returns true if the failure ratio is higher than x (with a minimum of 10 records)
60
+ def self.ratio_rule(ratio=0.1, minimum=10)
61
+ lambda { |list| list.length >= minimum and list.select(&:empty?).length.to_f / list.length >= ratio }
62
+ end
63
+
64
+ # returns true if the last x elements have failed
65
+ def self.sequence_rule(elements=5)
66
+ lambda { |list| list.length >= elements && list[0..elements-1].reject(&:empty?).length == 0 }
67
+ end
68
+
69
+ protected
70
+ def store(identifier, value)
71
+ redis.lpush(prefix(identifier), value)
72
+ redis.ltrim(prefix(identifier), 0, @list_length - 1)
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,3 @@
1
+ module SuccessTracker
2
+ VERSION = "0.5"
3
+ end
@@ -0,0 +1,22 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'success_tracker/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.homepage = "https://github.com/autohaus24/success_tracker"
8
+ gem.name = "success_tracker"
9
+ gem.version = SuccessTracker::VERSION
10
+ gem.authors = ["Michael Raidel"]
11
+ gem.email = ["m.raidel@autohaus24.de"]
12
+ gem.description = %q{SuccessTracker allows you to track success and failure of tasks}
13
+ gem.summary = %q{SuccessTracker allows you to track success and failure of tasks and define thresholds for unexpected failure conditions.}
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+ gem.add_dependency "redis"
20
+ gem.add_development_dependency "shoulda"
21
+ gem.add_development_dependency "rake"
22
+ end
@@ -0,0 +1,164 @@
1
+ require 'test/unit'
2
+ require 'shoulda'
3
+ require 'success_tracker'
4
+
5
+ class SuccessTracker::BaseTest < Test::Unit::TestCase
6
+ def setup
7
+ @redis = Redis.new
8
+ @prefix = "success_tracker"
9
+ @key = "test_key"
10
+ end
11
+
12
+ def teardown
13
+ @redis.del("#{@prefix}:#{@key}")
14
+ end
15
+
16
+ should "store a success in the given redis key" do
17
+ success_tracker = SuccessTracker::Base.new(@redis)
18
+ success_tracker.success(@key)
19
+
20
+ assert_equal ["1"], @redis.lrange("#{@prefix}:#{@key}", 0, -1)
21
+ end
22
+
23
+ should "store an error in the given redis key" do
24
+ success_tracker = SuccessTracker::Base.new(@redis)
25
+ success_tracker.failure(@key, :percent_10)
26
+
27
+ assert_equal [""], @redis.lrange("#{@prefix}:#{@key}", 0, -1)
28
+ end
29
+
30
+ should "yield the success callback on success" do
31
+ success_tracker = SuccessTracker::Base.new(@redis, :callbacks => { :success => lambda { |identifier| @identifier = "success: #{identifier}" } })
32
+ success_tracker.success(@key)
33
+
34
+ assert_equal "success: #{@key}", @identifier
35
+ end
36
+
37
+ should "yield the failure callback on failure" do
38
+ success_tracker = SuccessTracker::Base.new(@redis, :callbacks => { :failure => lambda { |identifier| @identifier = "failure: #{identifier}" } })
39
+ success_tracker.failure(@key, :percent_10)
40
+
41
+ assert_equal "failure: #{@key}", @identifier
42
+ end
43
+
44
+ should "raise an ArgumentError when initializing with unknown options" do
45
+ assert_raise(ArgumentError) do
46
+ SuccessTracker::Base.new(@redis, :foo => "bar")
47
+ end
48
+ end
49
+
50
+ should "allow a maximum number of records" do
51
+ success_tracker = SuccessTracker::Base.new(@redis)
52
+ 105.times { success_tracker.success(@key) }
53
+ assert_equal 100, @redis.lrange("#{@prefix}:#{@key}", 0, -1).length
54
+ end
55
+
56
+ should "yield failure block" do
57
+ success_tracker = SuccessTracker::Base.new(@redis)
58
+ success_tracker.failure(@key, :percent_10) do
59
+ @output_from_block = true
60
+ end
61
+
62
+ assert @output_from_block
63
+ end
64
+
65
+ should "track success" do
66
+ success_tracker = SuccessTracker::Base.new(@redis)
67
+ success_tracker.track(@key, :percent_10) { "working block" }
68
+ assert_equal ["1"], @redis.lrange("#{@prefix}:#{@key}", 0, -1)
69
+ end
70
+
71
+ should "return result from block on #track" do
72
+ success_tracker = SuccessTracker::Base.new(@redis)
73
+ assert_equal "result", success_tracker.track(@key, :percent_10) { "result" }
74
+ end
75
+
76
+ should "track failure and reraise tagged exception" do
77
+ success_tracker = SuccessTracker::Base.new(@redis)
78
+ begin
79
+ success_tracker.track(@key, :percent_10) { raise ArgumentError }
80
+ flunk "should have raised an exception before"
81
+ rescue => exception
82
+ assert ArgumentError === exception, "exception should be an ArgumentError"
83
+ assert SuccessTracker::NonSignificantError === exception, "exception should be a NonSignificantError"
84
+ end
85
+ assert_equal [""], @redis.lrange("#{@prefix}:#{@key}", 0, -1)
86
+ end
87
+
88
+ context "ratio_rule" do
89
+ should "return false until threshold of x percent is reached" do
90
+ rule = SuccessTracker::Base.ratio_rule(0.1)
91
+ assert !rule.call(["1"] * 10 + [""] * 1), "should return false when threshold is not reached"
92
+ assert rule.call(["1"] * 9 + [""] * 1), "should return true when threshold is reached"
93
+ end
94
+
95
+ should "always return true before minimum" do
96
+ rule = SuccessTracker::Base.ratio_rule(0.1, 3)
97
+ assert !rule.call([""] * 2), "should return false when below minimum"
98
+ assert rule.call([""] * 3), "should return true when minimum is reached"
99
+ end
100
+ end
101
+
102
+ context "sequence_rule" do
103
+ should "return false until threshold of x percent is reached" do
104
+ rule = SuccessTracker::Base.sequence_rule(5)
105
+ assert !rule.call([""] * 4 + ["1"] * 5), "should return false before having x failures in a row"
106
+ assert rule.call([""] * 5 + ["1"] * 5), "should return true when having x failures in a row"
107
+ end
108
+ end
109
+
110
+ context "over threshold" do
111
+ setup do
112
+ @success_tracker = SuccessTracker::Base.new(@redis, :rules => { :never_below_threshold => lambda { |list| true } })
113
+ end
114
+
115
+ should "reraise errors from failure block not extended with module" do
116
+ begin
117
+ @success_tracker.failure(@key, :never_below_threshold) do
118
+ raise NoMethodError
119
+ end
120
+ flunk "should raise exception before"
121
+ rescue NoMethodError => exception
122
+ assert !(SuccessTracker::NonSignificantError === exception), "should not have the module when meeting the error condition"
123
+ end
124
+ end
125
+
126
+ should "empty the list" do
127
+ @success_tracker.failure(@key, :never_below_threshold)
128
+ assert_equal [], @redis.lrange("#{@prefix}:#{@key}", 0, -1)
129
+ end
130
+ end
131
+
132
+ context "below threshold" do
133
+ setup do
134
+ @success_tracker = SuccessTracker::Base.new(@redis, :rules => { :always_below_threshold => lambda { |list| false } })
135
+ end
136
+
137
+ should "reraise errors from failure block extended with module" do
138
+ begin
139
+ @success_tracker.failure(@key, :always_below_threshold) do
140
+ raise NoMethodError
141
+ end
142
+ flunk "should raise exception before"
143
+ rescue NoMethodError => exception
144
+ assert SuccessTracker::NonSignificantError === exception, "should have the module before meeting the error condition"
145
+ end
146
+ end
147
+
148
+ should "not reraise errors from failure block extended with module" do
149
+ begin
150
+ @success_tracker.failure(@key, :always_below_threshold, :exceptions => [ ArgumentError ]) do
151
+ raise NoMethodError
152
+ end
153
+ flunk "should raise exception before"
154
+ rescue NoMethodError => exception
155
+ assert !(SuccessTracker::NonSignificantError === exception), "should not have the module for an NoMethodError"
156
+ end
157
+ end
158
+
159
+ should "not empty the list" do
160
+ @success_tracker.failure(@key, :always_below_threshold)
161
+ assert_equal [""], @redis.lrange("#{@prefix}:#{@key}", 0, -1)
162
+ end
163
+ end
164
+ end
metadata ADDED
@@ -0,0 +1,90 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: success_tracker
3
+ version: !ruby/object:Gem::Version
4
+ version: '0.5'
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Michael Raidel
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-03-28 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: redis
16
+ requirement: &86238180 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *86238180
25
+ - !ruby/object:Gem::Dependency
26
+ name: shoulda
27
+ requirement: &86237970 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: *86237970
36
+ - !ruby/object:Gem::Dependency
37
+ name: rake
38
+ requirement: &86237730 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :development
45
+ prerelease: false
46
+ version_requirements: *86237730
47
+ description: SuccessTracker allows you to track success and failure of tasks
48
+ email:
49
+ - m.raidel@autohaus24.de
50
+ executables: []
51
+ extensions: []
52
+ extra_rdoc_files: []
53
+ files:
54
+ - .gitignore
55
+ - Gemfile
56
+ - LICENSE.txt
57
+ - README.md
58
+ - Rakefile
59
+ - lib/success_tracker.rb
60
+ - lib/success_tracker/version.rb
61
+ - success_tracker.gemspec
62
+ - test/success_tracker_test.rb
63
+ homepage: https://github.com/autohaus24/success_tracker
64
+ licenses: []
65
+ post_install_message:
66
+ rdoc_options: []
67
+ require_paths:
68
+ - lib
69
+ required_ruby_version: !ruby/object:Gem::Requirement
70
+ none: false
71
+ requirements:
72
+ - - ! '>='
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ required_rubygems_version: !ruby/object:Gem::Requirement
76
+ none: false
77
+ requirements:
78
+ - - ! '>='
79
+ - !ruby/object:Gem::Version
80
+ version: '0'
81
+ requirements: []
82
+ rubyforge_project:
83
+ rubygems_version: 1.8.11
84
+ signing_key:
85
+ specification_version: 3
86
+ summary: SuccessTracker allows you to track success and failure of tasks and define
87
+ thresholds for unexpected failure conditions.
88
+ test_files:
89
+ - test/success_tracker_test.rb
90
+ has_rdoc: