prop 0.6.4 → 0.6.5
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +3 -0
- data/{README.rdoc → README.md} +42 -42
- data/lib/prop.rb +6 -5
- data/prop.gemspec +5 -4
- data/test/test_prop.rb +5 -3
- metadata +23 -22
data/Gemfile
ADDED
data/{README.rdoc → README.md}
RENAMED
@@ -1,96 +1,96 @@
|
|
1
1
|
|
2
|
-
|
2
|
+
# Prop [![Build Status](https://secure.travis-ci.org/morten/prop.png)](http://travis-ci.org/morten/prop)
|
3
3
|
|
4
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
5
|
|
6
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
7
|
|
8
|
-
|
9
|
-
|
10
|
-
|
8
|
+
Prop.read do |key|
|
9
|
+
Rails.cache.read(key)
|
10
|
+
end
|
11
11
|
|
12
|
-
|
13
|
-
|
14
|
-
|
12
|
+
Prop.write do |key, value|
|
13
|
+
Rails.cache.write(key, value)
|
14
|
+
end
|
15
15
|
|
16
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
17
|
|
18
|
-
|
18
|
+
## Defining thresholds
|
19
19
|
|
20
20
|
Once the read and write operations are defined, you can optionally define 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:
|
21
21
|
|
22
|
-
|
22
|
+
Prop.configure(:mails_per_hour, :threshold => 100, :interval => 1.hour, :description => "Mail rate limit exceeded")
|
23
23
|
|
24
24
|
You can now put the throttle to work with this values, by passing the "handle" to the respective methods in Prop:
|
25
25
|
|
26
|
-
|
27
|
-
|
26
|
+
# Throws Prop::RateLimitExceededError if the threshold/interval has been reached
|
27
|
+
Prop.throttle!(:mails_per_hour)
|
28
28
|
|
29
|
-
|
30
|
-
|
29
|
+
# Returns true if the threshold/interval has been reached
|
30
|
+
Prop.throttled?(:mails_per_hour)
|
31
31
|
|
32
|
-
|
33
|
-
|
32
|
+
# Sets the throttle "count" to 0
|
33
|
+
Prop.reset(:mails_per_hour)
|
34
34
|
|
35
|
-
|
36
|
-
|
35
|
+
# Returns the value of this throttle, usually a count, but see below for more
|
36
|
+
Prop.query(:mails_per_hour)
|
37
37
|
|
38
38
|
Prop will raise a RuntimeError if you attempt to operate on an undefined handle.
|
39
39
|
|
40
|
-
|
40
|
+
## Scoping a throttle
|
41
41
|
|
42
42
|
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:
|
43
43
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
44
|
+
Prop.throttle!(:mails_per_hour, mail.from)
|
45
|
+
Prop.throttled?(:mails_per_hour, mail.from)
|
46
|
+
Prop.reset(:mails_per_hour, mail.from)
|
47
|
+
Prop.query(:mails_per_hour, mail.from)
|
48
48
|
|
49
49
|
The throttle scope can also be an array of values, e.g.:
|
50
50
|
|
51
|
-
|
51
|
+
Prop.throttle!(:mails_per_hour, [ account.id, mail.from ])
|
52
52
|
|
53
|
-
|
53
|
+
## Error handling
|
54
54
|
|
55
55
|
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 and a "description" if specified during the configuration. The handle allows you to rescue Prop::RateLimitExceededError and differentiate action depending on the handle. For example, in Rails you can use this in e.g. ApplicationController:
|
56
56
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
57
|
+
rescue_from Prop::RateLimitExceededError do |e|
|
58
|
+
if e.handle == :authorization_attempt
|
59
|
+
render :status => :forbidden, :message => I18n.t(e.description)
|
60
|
+
elsif ...
|
61
|
+
|
62
|
+
end
|
62
63
|
end
|
63
|
-
end
|
64
64
|
|
65
|
-
|
65
|
+
## Disabling Prop
|
66
66
|
|
67
67
|
In case you need to perform e.g. a manual bulk operation:
|
68
68
|
|
69
|
-
|
70
|
-
|
71
|
-
|
69
|
+
Prop.disabled do
|
70
|
+
# No throttles will be tested here
|
71
|
+
end
|
72
72
|
|
73
|
-
|
73
|
+
## Threshold settings
|
74
74
|
|
75
75
|
You can chose to override the threshold for a given key:
|
76
76
|
|
77
|
-
|
77
|
+
Prop.throttle!(:mails_per_hour, mail.from, :threshold => current_account.mail_throttle_threshold)
|
78
78
|
|
79
79
|
When the threshold are invoked without argument, the key is nil and as such a scope of its own, i.e. these are equivalent:
|
80
80
|
|
81
|
-
|
82
|
-
|
81
|
+
Prop.throttle!(:mails_per_hour)
|
82
|
+
Prop.throttle!(:mails_per_hour, nil)
|
83
83
|
|
84
84
|
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:
|
85
85
|
|
86
|
-
|
87
|
-
|
86
|
+
Prop.setup(:execute_time, :threshold => 10, :interval => 1.minute)
|
87
|
+
Prop.throttle!(:execute_time, account.id, :increment => (Benchmark.realtime { execute }).to_i)
|
88
88
|
|
89
|
-
|
89
|
+
## How it works
|
90
90
|
|
91
91
|
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.
|
92
92
|
|
93
|
-
|
93
|
+
## Note on Patches/Pull Requests
|
94
94
|
|
95
95
|
* Fork the project.
|
96
96
|
* Make your feature addition or bug fix.
|
data/lib/prop.rb
CHANGED
@@ -7,7 +7,7 @@ class Object
|
|
7
7
|
end
|
8
8
|
|
9
9
|
class Prop
|
10
|
-
VERSION = "0.6.
|
10
|
+
VERSION = "0.6.5"
|
11
11
|
|
12
12
|
class RateLimitExceededError < RuntimeError
|
13
13
|
attr_accessor :handle, :retry_after, :description
|
@@ -103,10 +103,11 @@ class Prop
|
|
103
103
|
|
104
104
|
defaults = handles[handle]
|
105
105
|
return {
|
106
|
-
:key
|
107
|
-
:increment
|
108
|
-
:
|
109
|
-
:
|
106
|
+
:key => normalize_cache_key(key),
|
107
|
+
:increment => defaults[:increment],
|
108
|
+
:description => defaults[:description],
|
109
|
+
:threshold => defaults[:threshold].to_i,
|
110
|
+
:interval => defaults[:interval].to_i
|
110
111
|
}.merge(options)
|
111
112
|
end
|
112
113
|
|
data/prop.gemspec
CHANGED
@@ -13,8 +13,8 @@ Gem::Specification.new do |s|
|
|
13
13
|
## If your rubyforge_project name is different, then edit it and comment out
|
14
14
|
## the sub! line in the Rakefile
|
15
15
|
s.name = 'prop'
|
16
|
-
s.version = '0.6.
|
17
|
-
s.date = '2011-
|
16
|
+
s.version = '0.6.5'
|
17
|
+
s.date = '2011-12-13'
|
18
18
|
s.rubyforge_project = 'prop'
|
19
19
|
|
20
20
|
## Make sure your summary is short. The description may be as long
|
@@ -43,7 +43,7 @@ Gem::Specification.new do |s|
|
|
43
43
|
## Specify any RDoc options here. You'll want to add your README and
|
44
44
|
## LICENSE files to the extra_rdoc_files list.
|
45
45
|
s.rdoc_options = ["--charset=UTF-8"]
|
46
|
-
s.extra_rdoc_files = %w[README.
|
46
|
+
s.extra_rdoc_files = %w[README.md LICENSE]
|
47
47
|
|
48
48
|
## List your runtime dependencies here. Runtime dependencies are those
|
49
49
|
## that are needed for an end user to actually USE your code.
|
@@ -61,8 +61,9 @@ Gem::Specification.new do |s|
|
|
61
61
|
## THE MANIFEST COMMENTS, they are used as delimiters by the task.
|
62
62
|
# = MANIFEST =
|
63
63
|
s.files = %w[
|
64
|
+
Gemfile
|
64
65
|
LICENSE
|
65
|
-
README.
|
66
|
+
README.md
|
66
67
|
Rakefile
|
67
68
|
lib/prop.rb
|
68
69
|
prop.gemspec
|
data/test/test_prop.rb
CHANGED
@@ -152,15 +152,17 @@ class TestProp < Test::Unit::TestCase
|
|
152
152
|
end
|
153
153
|
|
154
154
|
should "raise Prop::RateLimitExceededError when the threshold is exceeded" do
|
155
|
+
Prop.configure(:hello, :threshold => 5, :interval => 10, :description => "Boom!")
|
156
|
+
|
155
157
|
5.times do |i|
|
156
|
-
Prop.throttle!(:hello, nil
|
158
|
+
Prop.throttle!(:hello, nil)
|
157
159
|
end
|
158
160
|
assert_raises(Prop::RateLimitExceededError) do
|
159
|
-
Prop.throttle!(:hello, nil
|
161
|
+
Prop.throttle!(:hello, nil)
|
160
162
|
end
|
161
163
|
|
162
164
|
begin
|
163
|
-
Prop.throttle!(:hello, nil
|
165
|
+
Prop.throttle!(:hello, nil)
|
164
166
|
fail
|
165
167
|
rescue Prop::RateLimitExceededError => e
|
166
168
|
assert_equal :hello, e.handle
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: prop
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 13
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 6
|
9
|
-
-
|
10
|
-
version: 0.6.
|
9
|
+
- 5
|
10
|
+
version: 0.6.5
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Morten Primdahl
|
@@ -15,13 +15,13 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2011-
|
18
|
+
date: 2011-12-13 00:00:00 -08:00
|
19
19
|
default_executable:
|
20
20
|
dependencies:
|
21
21
|
- !ruby/object:Gem::Dependency
|
22
22
|
name: rake
|
23
|
-
|
24
|
-
|
23
|
+
type: :development
|
24
|
+
version_requirements: &id001 !ruby/object:Gem::Requirement
|
25
25
|
none: false
|
26
26
|
requirements:
|
27
27
|
- - ">="
|
@@ -30,12 +30,12 @@ dependencies:
|
|
30
30
|
segments:
|
31
31
|
- 0
|
32
32
|
version: "0"
|
33
|
-
|
34
|
-
|
33
|
+
prerelease: false
|
34
|
+
requirement: *id001
|
35
35
|
- !ruby/object:Gem::Dependency
|
36
36
|
name: bundler
|
37
|
-
|
38
|
-
|
37
|
+
type: :development
|
38
|
+
version_requirements: &id002 !ruby/object:Gem::Requirement
|
39
39
|
none: false
|
40
40
|
requirements:
|
41
41
|
- - ">="
|
@@ -44,12 +44,12 @@ dependencies:
|
|
44
44
|
segments:
|
45
45
|
- 0
|
46
46
|
version: "0"
|
47
|
-
|
48
|
-
|
47
|
+
prerelease: false
|
48
|
+
requirement: *id002
|
49
49
|
- !ruby/object:Gem::Dependency
|
50
50
|
name: shoulda
|
51
|
-
|
52
|
-
|
51
|
+
type: :development
|
52
|
+
version_requirements: &id003 !ruby/object:Gem::Requirement
|
53
53
|
none: false
|
54
54
|
requirements:
|
55
55
|
- - ">="
|
@@ -58,12 +58,12 @@ dependencies:
|
|
58
58
|
segments:
|
59
59
|
- 0
|
60
60
|
version: "0"
|
61
|
-
|
62
|
-
|
61
|
+
prerelease: false
|
62
|
+
requirement: *id003
|
63
63
|
- !ruby/object:Gem::Dependency
|
64
64
|
name: mocha
|
65
|
-
|
66
|
-
|
65
|
+
type: :development
|
66
|
+
version_requirements: &id004 !ruby/object:Gem::Requirement
|
67
67
|
none: false
|
68
68
|
requirements:
|
69
69
|
- - ">="
|
@@ -72,8 +72,8 @@ dependencies:
|
|
72
72
|
segments:
|
73
73
|
- 0
|
74
74
|
version: "0"
|
75
|
-
|
76
|
-
|
75
|
+
prerelease: false
|
76
|
+
requirement: *id004
|
77
77
|
description: Gem for implementing rate limits.
|
78
78
|
email: primdahl@me.com
|
79
79
|
executables: []
|
@@ -81,11 +81,12 @@ executables: []
|
|
81
81
|
extensions: []
|
82
82
|
|
83
83
|
extra_rdoc_files:
|
84
|
-
- README.
|
84
|
+
- README.md
|
85
85
|
- LICENSE
|
86
86
|
files:
|
87
|
+
- Gemfile
|
87
88
|
- LICENSE
|
88
|
-
- README.
|
89
|
+
- README.md
|
89
90
|
- Rakefile
|
90
91
|
- lib/prop.rb
|
91
92
|
- prop.gemspec
|