glebpom-prop 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gemtest +0 -0
- data/LICENSE +20 -0
- data/README.rdoc +86 -0
- data/Rakefile +54 -0
- data/VERSION +1 -0
- data/lib/prop.rb +116 -0
- data/prop.gemspec +56 -0
- data/test/helper.rb +12 -0
- data/test/test_prop.rb +152 -0
- metadata +105 -0
data/.document
ADDED
data/.gemtest
ADDED
File without changes
|
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.
|
data/README.rdoc
ADDED
@@ -0,0 +1,86 @@
|
|
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.defaults(:mails_per_hour, :threshold => 100, :interval => 1.hour)
|
21
|
+
|
22
|
+
You can now put the throttle to work with this values, by passing the "handle" to the respective methods in Prop:
|
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.throttled?(:mails_per_hour)
|
29
|
+
|
30
|
+
# Sets the throttle "count" to 0
|
31
|
+
Prop.reset(:mails_per_hour)
|
32
|
+
|
33
|
+
# Returns the value of this throttle, usually a count, but see below for more
|
34
|
+
Prop.query(:mails_per_hour)
|
35
|
+
|
36
|
+
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:
|
37
|
+
|
38
|
+
Prop.throttle!(:mails_per_hour, mail.from)
|
39
|
+
Prop.throttled?(:mails_per_hour, mail.from)
|
40
|
+
Prop.reset(:mails_per_hour, mail.from)
|
41
|
+
Prop.query(:mails_per_hour, mail.from)
|
42
|
+
|
43
|
+
The throttle scope can also be an array of values, e.g.:
|
44
|
+
|
45
|
+
Prop.throttle!(:mails_per_hour, [ account.id, mail.from ])
|
46
|
+
|
47
|
+
If the throttle! method gets called more than "threshold" times within "interval in seconds" for a given handle and key combination, Prop throws a Prop::RateLimitExceededError. This exception contains a "handle" reference, which is handy when you are using Prop in multiple locations and want to be able to differentiate further up the stack. For example, in Rails you can use this in e.g. ApplicationController:
|
48
|
+
|
49
|
+
THROTTLE_MESSAGES = Hash.new("Throttle exceeded")
|
50
|
+
THROTTLE_MESSAGES[:login] = "Too many invalid login attempts. Try again later."
|
51
|
+
|
52
|
+
rescue_from Prop::RateLimitExceededError do |exception|
|
53
|
+
render :status => 403, :message => THROTTLE_MESSAGES[exception.handle]
|
54
|
+
end
|
55
|
+
|
56
|
+
You can chose to override the threshold for a given key:
|
57
|
+
|
58
|
+
Prop.throttle!(:mails_per_hour, mail.from, :threshold => account.mail_throttle_threshold)
|
59
|
+
|
60
|
+
When the threshold are invoked without argument, the key is nil and as such a scope of its own, i.e. these are equivalent:
|
61
|
+
|
62
|
+
Prop.throttle!(:mails_per_hour)
|
63
|
+
Prop.throttle!(:mails_per_hour, nil)
|
64
|
+
|
65
|
+
The default (and smallest possible) increment is 1, you can set that to any integer value using :increment which is handy for building time based throttles:
|
66
|
+
|
67
|
+
Prop.setup(:execute_time, :threshold => 10, :interval => 1.minute)
|
68
|
+
Prop.throttle!(:execute_time, account.id, :increment => (Benchmark.realtime { execute }).to_i)
|
69
|
+
|
70
|
+
== How it works
|
71
|
+
|
72
|
+
Prop uses the interval to define a window of time using simple div arithmetic. This means that it's a worst case throttle that will allow up to 2 times the specified requests within the specified interval.
|
73
|
+
|
74
|
+
== Note on Patches/Pull Requests
|
75
|
+
|
76
|
+
* Fork the project.
|
77
|
+
* Make your feature addition or bug fix.
|
78
|
+
* Add tests for it. This is important so I don't break it in a
|
79
|
+
future version unintentionally.
|
80
|
+
* Commit, do not mess with rakefile, version, or history.
|
81
|
+
(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)
|
82
|
+
* Send me a pull request. Bonus points for topic branches.
|
83
|
+
|
84
|
+
== Copyright
|
85
|
+
|
86
|
+
Copyright (c) 2010 Morten Primdahl. See LICENSE for details.
|
data/Rakefile
ADDED
@@ -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.5.1
|
data/lib/prop.rb
ADDED
@@ -0,0 +1,116 @@
|
|
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
|
+
attr_accessor :handle, :retry_after
|
12
|
+
|
13
|
+
def self.create(handle, key, threshold)
|
14
|
+
error = new("#{handle} threshold of #{threshold} exceeded for key '#{key}'")
|
15
|
+
error.handle = handle
|
16
|
+
error.retry_after = threshold - Time.now.to_i % threshold if threshold > 0
|
17
|
+
raise error
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class << self
|
22
|
+
attr_accessor :handles, :reader, :writer
|
23
|
+
|
24
|
+
def read(&blk)
|
25
|
+
self.reader = blk
|
26
|
+
end
|
27
|
+
|
28
|
+
def write(&blk)
|
29
|
+
self.writer = blk
|
30
|
+
end
|
31
|
+
|
32
|
+
def increment(&blk)
|
33
|
+
self.incrementer = blk
|
34
|
+
end
|
35
|
+
|
36
|
+
def incrementer=(value)
|
37
|
+
@incrementer = value
|
38
|
+
end
|
39
|
+
|
40
|
+
def incrementer
|
41
|
+
@incrementer ? Proc.new { |key, inc| @incrementer.call(key, inc) } :
|
42
|
+
Proc.new { |key, inc| self.writer.call(key, (self.reader.call(key) || 0).to_i + inc) }
|
43
|
+
end
|
44
|
+
|
45
|
+
def defaults(handle, defaults)
|
46
|
+
raise RuntimeError.new("Invalid threshold setting") unless defaults[:threshold].to_i > 0
|
47
|
+
raise RuntimeError.new("Invalid interval setting") unless defaults[:interval].to_i > 0
|
48
|
+
|
49
|
+
self.handles ||= {}
|
50
|
+
self.handles[handle] = defaults
|
51
|
+
end
|
52
|
+
|
53
|
+
def throttle!(handle, key = nil, options = {})
|
54
|
+
options = sanitized_prop_options(handle, key, options)
|
55
|
+
cache_key = sanitized_prop_key(key, options[:interval])
|
56
|
+
counter = reader.call(cache_key).to_i
|
57
|
+
|
58
|
+
incrementer.call(cache_key, [ 1, options[:increment].to_i ].max)
|
59
|
+
|
60
|
+
if counter >= options[:threshold]
|
61
|
+
raise Prop::RateLimitExceededError.create(handle, normalize_cache_key(key), options[:threshold])
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def throttled?(handle, key = nil, options = {})
|
66
|
+
options = sanitized_prop_options(handle, key, options)
|
67
|
+
cache_key = sanitized_prop_key(key, options[:interval])
|
68
|
+
|
69
|
+
reader.call(cache_key).to_i >= options[:threshold]
|
70
|
+
end
|
71
|
+
|
72
|
+
def reset(handle, key = nil, options = {})
|
73
|
+
options = sanitized_prop_options(handle, key, options)
|
74
|
+
cache_key = sanitized_prop_key(key, options[:interval])
|
75
|
+
|
76
|
+
writer.call(cache_key, 0)
|
77
|
+
end
|
78
|
+
|
79
|
+
def query(handle, key = nil, options = {})
|
80
|
+
options = sanitized_prop_options(handle, key, options)
|
81
|
+
cache_key = sanitized_prop_key(key, options[:interval])
|
82
|
+
|
83
|
+
reader.call(cache_key).to_i
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
# Builds the expiring cache key
|
89
|
+
def sanitized_prop_key(key, interval)
|
90
|
+
window = (Time.now.to_i / interval)
|
91
|
+
cache_key = "#{normalize_cache_key(key)}/#{ window }"
|
92
|
+
"prop/#{Digest::MD5.hexdigest(cache_key)}"
|
93
|
+
end
|
94
|
+
|
95
|
+
# Sanitizes the option set and sets defaults
|
96
|
+
def sanitized_prop_options(handle, key, options)
|
97
|
+
defaults = (handles || {})[handle] || {}
|
98
|
+
return {
|
99
|
+
:key => normalize_cache_key(key),
|
100
|
+
:increment => defaults[:increment],
|
101
|
+
:threshold => defaults[:threshold].to_i,
|
102
|
+
:interval => defaults[:interval].to_i
|
103
|
+
}.merge(options)
|
104
|
+
end
|
105
|
+
|
106
|
+
# Simple key expansion only supports arrays and primitives
|
107
|
+
def normalize_cache_key(key)
|
108
|
+
if key.is_a?(Array)
|
109
|
+
key.map { |part| normalize_cache_key(part) }.join('/')
|
110
|
+
else
|
111
|
+
key.to_s
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
116
|
+
end
|
data/prop.gemspec
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{glebpom-prop}
|
8
|
+
s.version = "0.0.1"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Gleb Pomykalov"]
|
12
|
+
s.date = %q{2011-02-03}
|
13
|
+
s.description = %q{A gem for implementing rate limiting}
|
14
|
+
s.email = %q{gleb.pomykalov@skype.net}
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"LICENSE",
|
17
|
+
"README.rdoc"
|
18
|
+
]
|
19
|
+
s.files = [
|
20
|
+
".document",
|
21
|
+
".gemtest",
|
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/glebpom/prop}
|
32
|
+
s.require_paths = ["lib"]
|
33
|
+
s.rubygems_version = %q{1.3.7}
|
34
|
+
s.summary = %q{Puts a cork in their requests}
|
35
|
+
s.test_files = [
|
36
|
+
"test/helper.rb",
|
37
|
+
"test/test_prop.rb"
|
38
|
+
]
|
39
|
+
|
40
|
+
if s.respond_to? :specification_version then
|
41
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
42
|
+
s.specification_version = 3
|
43
|
+
|
44
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
45
|
+
s.add_development_dependency(%q<shoulda>, [">= 0"])
|
46
|
+
s.add_development_dependency(%q<mocha>, [">= 0.9.8"])
|
47
|
+
else
|
48
|
+
s.add_dependency(%q<shoulda>, [">= 0"])
|
49
|
+
s.add_dependency(%q<mocha>, [">= 0.9.8"])
|
50
|
+
end
|
51
|
+
else
|
52
|
+
s.add_dependency(%q<shoulda>, [">= 0"])
|
53
|
+
s.add_dependency(%q<mocha>, [">= 0.9.8"])
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
data/test/helper.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'test/unit'
|
3
|
+
require 'shoulda'
|
4
|
+
require 'mocha'
|
5
|
+
require 'time'
|
6
|
+
|
7
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
8
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
9
|
+
require 'prop'
|
10
|
+
|
11
|
+
class Test::Unit::TestCase
|
12
|
+
end
|
data/test/test_prop.rb
ADDED
@@ -0,0 +1,152 @@
|
|
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
|
+
{"with incrementer" => lambda { Prop.increment { |key, inc| @store[key] ? @store[key] += inc : @store[key] = inc } },
|
16
|
+
"without incrementer" => lambda {} }.each do |desc, setup_block_for_context|
|
17
|
+
context desc do
|
18
|
+
setup do
|
19
|
+
instance_eval(&setup_block_for_context)
|
20
|
+
end
|
21
|
+
|
22
|
+
teardown do
|
23
|
+
Prop.instance_variable_set("@incrementer", nil)
|
24
|
+
end
|
25
|
+
|
26
|
+
context "#defaults" do
|
27
|
+
should "raise errors on invalid configuation" do
|
28
|
+
assert_raises(RuntimeError) do
|
29
|
+
Prop.defaults :hello_there, :threshold => 20, :interval => 'hello'
|
30
|
+
end
|
31
|
+
|
32
|
+
assert_raises(RuntimeError) do
|
33
|
+
Prop.defaults :hello_there, :threshold => 'wibble', :interval => 100
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
should "result in a default handle" do
|
38
|
+
Prop.defaults :hello_there, :threshold => 4, :interval => 10
|
39
|
+
4.times do |i|
|
40
|
+
assert_equal (i + 1), Prop.throttle!(:hello_there, 'some key')
|
41
|
+
end
|
42
|
+
|
43
|
+
assert_raises(Prop::RateLimitExceededError) { Prop.throttle!(:hello_there, 'some key') }
|
44
|
+
assert_equal 5, Prop.throttle!(:hello_there, 'some key', :threshold => 20)
|
45
|
+
end
|
46
|
+
|
47
|
+
should "create a handle accepts various cache key types" do
|
48
|
+
Prop.defaults :hello_there, :threshold => 4, :interval => 10
|
49
|
+
assert_equal 1, Prop.throttle!(:hello_there, 5)
|
50
|
+
assert_equal 2, Prop.throttle!(:hello_there, 5)
|
51
|
+
assert_equal 1, Prop.throttle!(:hello_there, '6')
|
52
|
+
assert_equal 2, Prop.throttle!(:hello_there, '6')
|
53
|
+
assert_equal 1, Prop.throttle!(:hello_there, [ 5, '6' ])
|
54
|
+
assert_equal 2, Prop.throttle!(:hello_there, [ 5, '6' ])
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
context "#reset" do
|
59
|
+
setup do
|
60
|
+
Prop.defaults :hello, :threshold => 10, :interval => 10
|
61
|
+
|
62
|
+
5.times do |i|
|
63
|
+
assert_equal (i + 1), Prop.throttle!(:hello)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
should "set the correct counter to 0" do
|
68
|
+
Prop.throttle!(:hello, 'wibble')
|
69
|
+
Prop.throttle!(:hello, 'wibble')
|
70
|
+
|
71
|
+
Prop.reset(:hello)
|
72
|
+
assert_equal 1, Prop.throttle!(:hello)
|
73
|
+
|
74
|
+
assert_equal 3, Prop.throttle!(:hello, 'wibble')
|
75
|
+
Prop.reset(:hello, 'wibble')
|
76
|
+
assert_equal 1, Prop.throttle!(:hello, 'wibble')
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
context "#throttled?" do
|
81
|
+
should "return true once the threshold has been reached" do
|
82
|
+
Prop.defaults(:hello, :threshold => 2, :interval => 10)
|
83
|
+
Prop.throttle!(:hello)
|
84
|
+
assert !Prop.throttled?(:hello)
|
85
|
+
Prop.throttle!(:hello)
|
86
|
+
assert Prop.throttled?(:hello)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
context "#throttle!" do
|
91
|
+
should "increment counter correctly" do
|
92
|
+
3.times do |i|
|
93
|
+
assert_equal (i + 1), Prop.throttle!(:hello, nil, :threshold => 10, :interval => 10)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
should "reset counter when time window is passed" do
|
98
|
+
3.times do |i|
|
99
|
+
assert_equal (i + 1), Prop.throttle!(:hello, nil, :threshold => 10, :interval => 10)
|
100
|
+
end
|
101
|
+
|
102
|
+
Time.stubs(:now).returns(@start + 20)
|
103
|
+
|
104
|
+
3.times do |i|
|
105
|
+
assert_equal (i + 1), Prop.throttle!(:hello, nil, :threshold => 10, :interval => 10)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
should "not increment the counter beyond the threshold" do
|
110
|
+
Prop.defaults(:hello, :threshold => 5, :interval => 1)
|
111
|
+
10.times do |i|
|
112
|
+
Prop.throttle!(:hello) rescue nil
|
113
|
+
end
|
114
|
+
|
115
|
+
assert_equal 5, Prop.query(:hello)
|
116
|
+
end
|
117
|
+
|
118
|
+
should "support custom increments" do
|
119
|
+
Prop.defaults(:hello, :threshold => 100, :interval => 10)
|
120
|
+
|
121
|
+
Prop.throttle!(:hello)
|
122
|
+
Prop.throttle!(:hello)
|
123
|
+
|
124
|
+
assert_equal 2, Prop.query(:hello)
|
125
|
+
|
126
|
+
Prop.throttle!(:hello, nil, :increment => 48)
|
127
|
+
|
128
|
+
assert_equal 50, Prop.query(:hello)
|
129
|
+
end
|
130
|
+
|
131
|
+
should "raise Prop::RateLimitExceededError when the threshold is exceeded" do
|
132
|
+
5.times do |i|
|
133
|
+
Prop.throttle!(:hello, nil, :threshold => 5, :interval => 10)
|
134
|
+
end
|
135
|
+
assert_raises(Prop::RateLimitExceededError) do
|
136
|
+
Prop.throttle!(:hello, nil, :threshold => 5, :interval => 10)
|
137
|
+
end
|
138
|
+
|
139
|
+
begin
|
140
|
+
Prop.throttle!(:hello, nil, :threshold => 5, :interval => 10)
|
141
|
+
fail
|
142
|
+
rescue Prop::RateLimitExceededError => e
|
143
|
+
assert_equal :hello, e.handle
|
144
|
+
assert_equal "hello threshold of 5 exceeded for key ''", e.message
|
145
|
+
assert e.retry_after
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
metadata
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: glebpom-prop
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 29
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 1
|
10
|
+
version: 0.0.1
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Gleb Pomykalov
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-02-03 00:00:00 Z
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: shoulda
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ">="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
hash: 3
|
29
|
+
segments:
|
30
|
+
- 0
|
31
|
+
version: "0"
|
32
|
+
type: :development
|
33
|
+
version_requirements: *id001
|
34
|
+
- !ruby/object:Gem::Dependency
|
35
|
+
name: mocha
|
36
|
+
prerelease: false
|
37
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
38
|
+
none: false
|
39
|
+
requirements:
|
40
|
+
- - ">="
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
hash: 43
|
43
|
+
segments:
|
44
|
+
- 0
|
45
|
+
- 9
|
46
|
+
- 8
|
47
|
+
version: 0.9.8
|
48
|
+
type: :development
|
49
|
+
version_requirements: *id002
|
50
|
+
description: A gem for implementing rate limiting
|
51
|
+
email: gleb.pomykalov@skype.net
|
52
|
+
executables: []
|
53
|
+
|
54
|
+
extensions: []
|
55
|
+
|
56
|
+
extra_rdoc_files:
|
57
|
+
- LICENSE
|
58
|
+
- README.rdoc
|
59
|
+
files:
|
60
|
+
- .document
|
61
|
+
- .gemtest
|
62
|
+
- LICENSE
|
63
|
+
- README.rdoc
|
64
|
+
- Rakefile
|
65
|
+
- VERSION
|
66
|
+
- lib/prop.rb
|
67
|
+
- prop.gemspec
|
68
|
+
- test/helper.rb
|
69
|
+
- test/test_prop.rb
|
70
|
+
homepage: http://github.com/glebpom/prop
|
71
|
+
licenses: []
|
72
|
+
|
73
|
+
post_install_message:
|
74
|
+
rdoc_options: []
|
75
|
+
|
76
|
+
require_paths:
|
77
|
+
- lib
|
78
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
79
|
+
none: false
|
80
|
+
requirements:
|
81
|
+
- - ">="
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
hash: 3
|
84
|
+
segments:
|
85
|
+
- 0
|
86
|
+
version: "0"
|
87
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
88
|
+
none: false
|
89
|
+
requirements:
|
90
|
+
- - ">="
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
hash: 3
|
93
|
+
segments:
|
94
|
+
- 0
|
95
|
+
version: "0"
|
96
|
+
requirements: []
|
97
|
+
|
98
|
+
rubyforge_project:
|
99
|
+
rubygems_version: 1.8.6
|
100
|
+
signing_key:
|
101
|
+
specification_version: 3
|
102
|
+
summary: Puts a cork in their requests
|
103
|
+
test_files:
|
104
|
+
- test/helper.rb
|
105
|
+
- test/test_prop.rb
|