prop 0.3.1

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 Morten Primdahl
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,68 @@
1
+
2
+ = Prop
3
+
4
+ Prop is a simple gem for rate limiting requests of any kind. It allows you to configure hooks for registering certain actions, such that you can define thresholds, register usage and finally act on exceptions once thresholds get exceeded.
5
+
6
+ To get going with Prop you first define the read and write operations. These define how you write a registered request and how to read the number of requests for a given action. For example do something like the below in a Rails initializer:
7
+
8
+ Prop.read do |key|
9
+ Rails.cache.read(key)
10
+ end
11
+
12
+ Prop.write do |key, value|
13
+ Rails.cache.write(key, value)
14
+ end
15
+
16
+ You can choose to rely on a database or Moneta or Redis or whatever you'd like to use for transient storage. Prop does not do any sort of clean up of its key space, so you would have to implement that manually should you be using anything but an LRU cache.
17
+
18
+ Once the read and write operations are defined, you can optionally define some preconfigured default thresholds. If for example, you want to have a threshold on accepted emails per hour from a given user, you could define a threshold and interval (in seconds) for this like so:
19
+
20
+ Prop.setup(:mails_per_hour, :threshold => 100, :interval => 1.hour)
21
+
22
+ This results in a the following methods being generated:
23
+
24
+ # Throws Prop::RateLimitExceededError if the threshold/interval has been reached
25
+ Prop.throttle_mails_per_hour!
26
+
27
+ # Returns true if the threshold/interval has been reached
28
+ Prop.throttle_mails_per_hour?
29
+
30
+ # Sets the counter to 0
31
+ Prop.reset_mails_per_hour
32
+
33
+ In many cases you will want to tie a specific key to a defined throttle, for example you can scope the throttling to a specific sender rather than running a global "mails per hour" throttle:
34
+
35
+ Prop.throttle_mails_per_hour!(mail.from)
36
+
37
+ If this method gets called more than "threshold" times within "interval in seconds" Prop throws a Prop::RateLimitExceededError.
38
+
39
+ You can chose to override the threshold for a given key:
40
+
41
+ Prop.mails_per_hour(mail.from, :threshold => account.mail_throttle_threshold)
42
+
43
+ If you wish to reset a specific throttle, you can do that like so:
44
+
45
+ Prop.reset_mails_per_hour(mail.from)
46
+
47
+ When the threshold are invoked without argument, the key is nil and as such a scope of its own.
48
+
49
+ Lastly you can use Prop without registering the thresholds up front:
50
+
51
+ Prop.throttle!(:key => 'nuisance@example.com', :threshold => 100, :interval -> 1.hour)
52
+ Prop.reset(:key => 'nuisance@example.com', :threshold => 100, :interval -> 1.hour)
53
+
54
+ It's up to you to pass an appropriate key which reflects the scope you're rate limiting. The interval is tied to the underlying key generating mechanism, so if you change that between calls and have all other things equal, then that will result in different throttles being set.
55
+
56
+ == Note on Patches/Pull Requests
57
+
58
+ * Fork the project.
59
+ * Make your feature addition or bug fix.
60
+ * Add tests for it. This is important so I don't break it in a
61
+ future version unintentionally.
62
+ * Commit, do not mess with rakefile, version, or history.
63
+ (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)
64
+ * Send me a pull request. Bonus points for topic branches.
65
+
66
+ == Copyright
67
+
68
+ Copyright (c) 2010 Morten Primdahl. See LICENSE for details.
@@ -0,0 +1,54 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "prop"
8
+ gem.summary = %Q{Puts a cork in their requests}
9
+ gem.description = %Q{A gem for implementing rate limiting}
10
+ gem.email = "morten@zendesk.com"
11
+ gem.homepage = "http://github.com/morten/prop"
12
+ gem.authors = ["Morten Primdahl"]
13
+ gem.add_development_dependency "shoulda", ">= 0"
14
+ gem.add_development_dependency "mocha", ">= 0.9.8"
15
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
16
+ end
17
+ Jeweler::GemcutterTasks.new
18
+ rescue LoadError
19
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
20
+ end
21
+
22
+ require 'rake/testtask'
23
+ Rake::TestTask.new(:test) do |test|
24
+ test.libs << 'lib' << 'test'
25
+ test.pattern = 'test/**/test_*.rb'
26
+ test.verbose = true
27
+ end
28
+
29
+ begin
30
+ require 'rcov/rcovtask'
31
+ Rcov::RcovTask.new do |test|
32
+ test.libs << 'test'
33
+ test.pattern = 'test/**/test_*.rb'
34
+ test.verbose = true
35
+ end
36
+ rescue LoadError
37
+ task :rcov do
38
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
39
+ end
40
+ end
41
+
42
+ task :test => :check_dependencies
43
+
44
+ task :default => :test
45
+
46
+ require 'rake/rdoctask'
47
+ Rake::RDocTask.new do |rdoc|
48
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
49
+
50
+ rdoc.rdoc_dir = 'rdoc'
51
+ rdoc.title = "prop #{version}"
52
+ rdoc.rdoc_files.include('README*')
53
+ rdoc.rdoc_files.include('lib/**/*.rb')
54
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.3.1
@@ -0,0 +1,83 @@
1
+ require 'digest/md5'
2
+
3
+ class Object
4
+ def define_prop_class_method(name, &blk)
5
+ (class << self; self; end).instance_eval { define_method(name, &blk) }
6
+ end
7
+ end
8
+
9
+ class Prop
10
+ class RateLimitExceededError < RuntimeError
11
+ end
12
+
13
+ class << self
14
+ attr_accessor :handles, :reader, :writer
15
+
16
+ def read(&blk)
17
+ self.reader = blk
18
+ end
19
+
20
+ def write(&blk)
21
+ self.writer = blk
22
+ end
23
+
24
+ def setup(handle, defaults)
25
+ raise RuntimeError.new("Invalid threshold setting") unless defaults[:threshold].to_i > 0
26
+ raise RuntimeError.new("Invalid interval setting") unless defaults[:interval].to_i > 0
27
+
28
+ define_prop_class_method "throttle_#{handle}!" do |*args|
29
+ throttle!(sanitized_prop_options(handle, args, defaults))
30
+ end
31
+
32
+ define_prop_class_method "throttle_#{handle}?" do |*args|
33
+ throttle?(sanitized_prop_options(handle, args, defaults))
34
+ end
35
+
36
+ define_prop_class_method "reset_#{handle}" do |*args|
37
+ reset(sanitized_prop_options(handle, args, defaults))
38
+ end
39
+ end
40
+
41
+ def throttle?(options)
42
+ cache_key = sanitized_prop_key(options)
43
+ reader.call(cache_key).to_i >= options[:threshold]
44
+ end
45
+
46
+ def throttle!(options)
47
+ cache_key = sanitized_prop_key(options)
48
+ counter = reader.call(cache_key).to_i
49
+
50
+ if counter >= options[:threshold]
51
+ raise Prop::RateLimitExceededError.new("#{options[:key]} threshold #{options[:threshold]} exceeded")
52
+ else
53
+ writer.call(cache_key, counter + 1)
54
+ end
55
+ end
56
+
57
+ def reset(options)
58
+ cache_key = sanitized_prop_key(options)
59
+ writer.call(cache_key, 0)
60
+ end
61
+
62
+ def count(options)
63
+ cache_key = sanitized_prop_key(options)
64
+ reader.call(cache_key).to_i
65
+ end
66
+
67
+ private
68
+
69
+ def sanitized_prop_key(options)
70
+ cache_key = "#{options[:key]}/#{Time.now.to_i / options[:interval]}"
71
+ "prop/#{Digest::MD5.hexdigest(cache_key)}"
72
+ end
73
+
74
+ def sanitized_prop_options(handle, args, defaults)
75
+ key = handle.to_s
76
+ key << "/#{args.first}" if args.first
77
+
78
+ options = { :key => key, :threshold => defaults[:threshold].to_i, :interval => defaults[:interval].to_i }
79
+ options = options.merge(args.last) if args.last.is_a?(Hash)
80
+ options
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,57 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{prop}
8
+ s.version = "0.3.1"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Morten Primdahl"]
12
+ s.date = %q{2010-07-21}
13
+ s.description = %q{A gem for implementing rate limiting}
14
+ s.email = %q{morten@zendesk.com}
15
+ s.extra_rdoc_files = [
16
+ "LICENSE",
17
+ "README.rdoc"
18
+ ]
19
+ s.files = [
20
+ ".document",
21
+ ".gitignore",
22
+ "LICENSE",
23
+ "README.rdoc",
24
+ "Rakefile",
25
+ "VERSION",
26
+ "lib/prop.rb",
27
+ "prop.gemspec",
28
+ "test/helper.rb",
29
+ "test/test_prop.rb"
30
+ ]
31
+ s.homepage = %q{http://github.com/morten/prop}
32
+ s.rdoc_options = ["--charset=UTF-8"]
33
+ s.require_paths = ["lib"]
34
+ s.rubygems_version = %q{1.3.7}
35
+ s.summary = %q{Puts a cork in their requests}
36
+ s.test_files = [
37
+ "test/helper.rb",
38
+ "test/test_prop.rb"
39
+ ]
40
+
41
+ if s.respond_to? :specification_version then
42
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
43
+ s.specification_version = 3
44
+
45
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
46
+ s.add_development_dependency(%q<shoulda>, [">= 0"])
47
+ s.add_development_dependency(%q<mocha>, [">= 0.9.8"])
48
+ else
49
+ s.add_dependency(%q<shoulda>, [">= 0"])
50
+ s.add_dependency(%q<mocha>, [">= 0.9.8"])
51
+ end
52
+ else
53
+ s.add_dependency(%q<shoulda>, [">= 0"])
54
+ s.add_dependency(%q<mocha>, [">= 0.9.8"])
55
+ end
56
+ end
57
+
@@ -0,0 +1,11 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'shoulda'
4
+ require 'mocha'
5
+
6
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
7
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
8
+ require 'prop'
9
+
10
+ class Test::Unit::TestCase
11
+ end
@@ -0,0 +1,126 @@
1
+ require 'helper'
2
+
3
+ class TestProp < Test::Unit::TestCase
4
+
5
+ context "Prop" do
6
+ setup do
7
+ store = {}
8
+ Prop.read { |key| store[key] }
9
+ Prop.write { |key, value| store[key] = value }
10
+
11
+ @start = Time.now
12
+ Time.stubs(:now).returns(@start)
13
+ end
14
+
15
+ context "#configure" do
16
+ should "raise errors on invalid configuation" do
17
+ assert_raises(RuntimeError) do
18
+ Prop.setup :hello_there, :threshold => 20, :interval => 'hello'
19
+ end
20
+
21
+ assert_raises(RuntimeError) do
22
+ Prop.setup :hello_there, :threshold => 'wibble', :interval => 100
23
+ end
24
+ end
25
+
26
+ should "accept a handle and an options hash" do
27
+ Prop.setup :hello_there, :threshold => 40, :interval => 100
28
+ assert Prop.respond_to?(:throttle_hello_there!)
29
+ end
30
+
31
+ should "result in a default handle" do
32
+ Prop.setup :hello_there, :threshold => 4, :interval => 10
33
+ 4.times do |i|
34
+ assert_equal (i + 1), Prop.throttle_hello_there!('some key')
35
+ end
36
+
37
+ assert_raises(Prop::RateLimitExceededError) { Prop.throttle_hello_there!('some key') }
38
+ assert_equal 5, Prop.throttle_hello_there!('some key', :threshold => 20)
39
+ end
40
+
41
+ should "create a handle accepts integer keys" do
42
+ Prop.setup :hello_there, :threshold => 4, :interval => 10
43
+ assert Prop.throttle_hello_there!(5)
44
+ end
45
+
46
+ should "not shadow undefined methods" do
47
+ assert_raises(NoMethodError) { Prop.no_such_handle }
48
+ end
49
+ end
50
+
51
+ context "#reset" do
52
+ setup do
53
+ Prop.setup :hello, :threshold => 10, :interval => 10
54
+
55
+ 5.times do |i|
56
+ assert_equal (i + 1), Prop.throttle_hello!
57
+ end
58
+ end
59
+
60
+ should "set the correct counter to 0" do
61
+ Prop.throttle_hello!('wibble')
62
+ Prop.throttle_hello!('wibble')
63
+
64
+ Prop.reset_hello
65
+ assert_equal 1, Prop.throttle_hello!
66
+
67
+ assert_equal 3, Prop.throttle_hello!('wibble')
68
+ Prop.reset_hello('wibble')
69
+ assert_equal 1, Prop.throttle_hello!('wibble')
70
+ end
71
+
72
+ should "be directly invokable" do
73
+ Prop.reset :key => :hello, :threshold => 10, :interval => 10
74
+ assert_equal 1, Prop.throttle_hello!
75
+ end
76
+ end
77
+
78
+ context "#throttle?" do
79
+ should "return true once the threshold has been reached" do
80
+ Prop.throttle!(:key => 'hello', :threshold => 2, :interval => 10)
81
+ assert !Prop.throttle?(:key => 'hello', :threshold => 2, :interval => 10)
82
+
83
+ Prop.throttle!(:key => 'hello', :threshold => 2, :interval => 10)
84
+ assert Prop.throttle?(:key => 'hello', :threshold => 2, :interval => 10)
85
+ end
86
+ end
87
+
88
+ context "#throttle!" do
89
+ should "increment counter correctly" do
90
+ 3.times do |i|
91
+ assert_equal (i + 1), Prop.throttle!(:key => 'hello', :threshold => 10, :interval => 10)
92
+ end
93
+ end
94
+
95
+ should "reset counter when time window is passed" do
96
+ 3.times do |i|
97
+ assert_equal (i + 1), Prop.throttle!(:key => 'hello', :threshold => 10, :interval => 10)
98
+ end
99
+
100
+ Time.stubs(:now).returns(@start + 20)
101
+
102
+ 3.times do |i|
103
+ assert_equal (i + 1), Prop.throttle!(:key => 'hello', :threshold => 10, :interval => 10)
104
+ end
105
+ end
106
+
107
+ should "not increment the counter beyon the threshold" do
108
+ 10.times do |i|
109
+ Prop.throttle!(:key => 'hello', :threshold => 5, :interval => 10) rescue nil
110
+ end
111
+
112
+ assert_equal 5, Prop.count(:key => 'hello', :threshold => 5, :interval => 10)
113
+ end
114
+
115
+ should "raise Prop::RateLimitExceededError when the threshold is exceeded" do
116
+ 5.times do |i|
117
+ Prop.throttle!(:key => 'hello', :threshold => 5, :interval => 10)
118
+ end
119
+ assert_raises(Prop::RateLimitExceededError) do
120
+ Prop.throttle!(:key => 'hello', :threshold => 5, :interval => 10)
121
+ end
122
+ end
123
+ end
124
+
125
+ end
126
+ end
metadata ADDED
@@ -0,0 +1,107 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: prop
3
+ version: !ruby/object:Gem::Version
4
+ hash: 17
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 3
9
+ - 1
10
+ version: 0.3.1
11
+ platform: ruby
12
+ authors:
13
+ - Morten Primdahl
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2010-07-21 00:00:00 -07:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: shoulda
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 3
30
+ segments:
31
+ - 0
32
+ version: "0"
33
+ type: :development
34
+ version_requirements: *id001
35
+ - !ruby/object:Gem::Dependency
36
+ name: mocha
37
+ prerelease: false
38
+ requirement: &id002 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ hash: 43
44
+ segments:
45
+ - 0
46
+ - 9
47
+ - 8
48
+ version: 0.9.8
49
+ type: :development
50
+ version_requirements: *id002
51
+ description: A gem for implementing rate limiting
52
+ email: morten@zendesk.com
53
+ executables: []
54
+
55
+ extensions: []
56
+
57
+ extra_rdoc_files:
58
+ - LICENSE
59
+ - README.rdoc
60
+ files:
61
+ - .document
62
+ - .gitignore
63
+ - LICENSE
64
+ - README.rdoc
65
+ - Rakefile
66
+ - VERSION
67
+ - lib/prop.rb
68
+ - prop.gemspec
69
+ - test/helper.rb
70
+ - test/test_prop.rb
71
+ has_rdoc: true
72
+ homepage: http://github.com/morten/prop
73
+ licenses: []
74
+
75
+ post_install_message:
76
+ rdoc_options:
77
+ - --charset=UTF-8
78
+ require_paths:
79
+ - lib
80
+ required_ruby_version: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ hash: 3
86
+ segments:
87
+ - 0
88
+ version: "0"
89
+ required_rubygems_version: !ruby/object:Gem::Requirement
90
+ none: false
91
+ requirements:
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ hash: 3
95
+ segments:
96
+ - 0
97
+ version: "0"
98
+ requirements: []
99
+
100
+ rubyforge_project:
101
+ rubygems_version: 1.3.7
102
+ signing_key:
103
+ specification_version: 3
104
+ summary: Puts a cork in their requests
105
+ test_files:
106
+ - test/helper.rb
107
+ - test/test_prop.rb