prop 0.6.1 → 0.6.2
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +26 -7
- data/VERSION +1 -1
- data/lib/prop.rb +8 -4
- data/prop.gemspec +2 -2
- data/test/test_prop.rb +8 -1
- metadata +4 -4
data/README.rdoc
CHANGED
@@ -15,9 +15,11 @@ To get going with Prop you first define the read and write operations. These def
|
|
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
|
+
== Defining thresholds
|
19
|
+
|
18
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:
|
19
21
|
|
20
|
-
Prop.configure(:mails_per_hour, :threshold => 100, :interval => 1.hour)
|
22
|
+
Prop.configure(:mails_per_hour, :threshold => 100, :interval => 1.hour, :description => "Mail rate limit exceeded")
|
21
23
|
|
22
24
|
You can now put the throttle to work with this values, by passing the "handle" to the respective methods in Prop:
|
23
25
|
|
@@ -33,6 +35,10 @@ You can now put the throttle to work with this values, by passing the "handle" t
|
|
33
35
|
# Returns the value of this throttle, usually a count, but see below for more
|
34
36
|
Prop.query(:mails_per_hour)
|
35
37
|
|
38
|
+
Prop will raise a RuntimeError if you attempt to operate on an undefined handle.
|
39
|
+
|
40
|
+
== Scoping a throttle
|
41
|
+
|
36
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:
|
37
43
|
|
38
44
|
Prop.throttle!(:mails_per_hour, mail.from)
|
@@ -44,18 +50,31 @@ The throttle scope can also be an array of values, e.g.:
|
|
44
50
|
|
45
51
|
Prop.throttle!(:mails_per_hour, [ account.id, mail.from ])
|
46
52
|
|
47
|
-
|
53
|
+
== Error handling
|
48
54
|
|
49
|
-
|
50
|
-
THROTTLE_MESSAGES[:login] = "Too many invalid login attempts. Try again later."
|
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:
|
51
56
|
|
52
|
-
rescue_from Prop::RateLimitExceededError do |
|
53
|
-
|
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
|
54
63
|
end
|
55
64
|
|
65
|
+
== Disabling Prop
|
66
|
+
|
67
|
+
In case you need to perform e.g. a manual bulk operation:
|
68
|
+
|
69
|
+
Prop.disabled do
|
70
|
+
# No throttles will be tested here
|
71
|
+
end
|
72
|
+
|
73
|
+
== Threshold settings
|
74
|
+
|
56
75
|
You can chose to override the threshold for a given key:
|
57
76
|
|
58
|
-
Prop.throttle!(:mails_per_hour, mail.from, :threshold =>
|
77
|
+
Prop.throttle!(:mails_per_hour, mail.from, :threshold => current_account.mail_throttle_threshold)
|
59
78
|
|
60
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:
|
61
80
|
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.6.
|
1
|
+
0.6.2
|
data/lib/prop.rb
CHANGED
@@ -8,12 +8,14 @@ end
|
|
8
8
|
|
9
9
|
class Prop
|
10
10
|
class RateLimitExceededError < RuntimeError
|
11
|
-
attr_accessor :handle, :retry_after
|
11
|
+
attr_accessor :handle, :retry_after, :description
|
12
12
|
|
13
|
-
def self.create(handle, key, threshold)
|
13
|
+
def self.create(handle, key, threshold, description = nil)
|
14
14
|
error = new("#{handle} threshold of #{threshold} exceeded for key '#{key}'")
|
15
|
+
error.description = description
|
15
16
|
error.handle = handle
|
16
17
|
error.retry_after = threshold - Time.now.to_i % threshold if threshold > 0
|
18
|
+
|
17
19
|
raise error
|
18
20
|
end
|
19
21
|
end
|
@@ -56,7 +58,7 @@ class Prop
|
|
56
58
|
return counter if disabled?
|
57
59
|
|
58
60
|
if counter >= options[:threshold]
|
59
|
-
raise Prop::RateLimitExceededError.create(handle, normalize_cache_key(key), options[:threshold])
|
61
|
+
raise Prop::RateLimitExceededError.create(handle, normalize_cache_key(key), options[:threshold], options[:description])
|
60
62
|
else
|
61
63
|
writer.call(cache_key, counter + [ 1, options[:increment].to_i ].max)
|
62
64
|
end
|
@@ -94,7 +96,9 @@ class Prop
|
|
94
96
|
|
95
97
|
# Sanitizes the option set and sets defaults
|
96
98
|
def sanitized_prop_options(handle, key, options)
|
97
|
-
|
99
|
+
raise RuntimeError.new("No such handle configured: #{handle.inspect}") if handles.nil? || handles[handle].nil?
|
100
|
+
|
101
|
+
defaults = handles[handle]
|
98
102
|
return {
|
99
103
|
:key => normalize_cache_key(key),
|
100
104
|
:increment => defaults[:increment],
|
data/prop.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{prop}
|
8
|
-
s.version = "0.6.
|
8
|
+
s.version = "0.6.2"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Morten Primdahl"]
|
12
|
-
s.date = %q{2011-08-
|
12
|
+
s.date = %q{2011-08-22}
|
13
13
|
s.description = %q{A gem for implementing rate limiting}
|
14
14
|
s.email = %q{morten@zendesk.com}
|
15
15
|
s.extra_rdoc_files = [
|
data/test/test_prop.rb
CHANGED
@@ -144,14 +144,21 @@ class TestProp < Test::Unit::TestCase
|
|
144
144
|
end
|
145
145
|
|
146
146
|
begin
|
147
|
-
Prop.throttle!(:hello, nil, :threshold => 5, :interval => 10)
|
147
|
+
Prop.throttle!(:hello, nil, :threshold => 5, :interval => 10, :description => "Boom!")
|
148
148
|
fail
|
149
149
|
rescue Prop::RateLimitExceededError => e
|
150
150
|
assert_equal :hello, e.handle
|
151
151
|
assert_equal "hello threshold of 5 exceeded for key ''", e.message
|
152
|
+
assert_equal "Boom!", e.description
|
152
153
|
assert e.retry_after
|
153
154
|
end
|
154
155
|
end
|
156
|
+
|
157
|
+
should "raise a RuntimeError when a handle has not been configured" do
|
158
|
+
assert_raises(RuntimeError) do
|
159
|
+
Prop.throttle!(:no_such_handle, nil, :threshold => 5, :interval => 10)
|
160
|
+
end
|
161
|
+
end
|
155
162
|
end
|
156
163
|
|
157
164
|
end
|
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: 3
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 6
|
9
|
-
-
|
10
|
-
version: 0.6.
|
9
|
+
- 2
|
10
|
+
version: 0.6.2
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Morten Primdahl
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2011-08-
|
18
|
+
date: 2011-08-22 00:00:00 -07:00
|
19
19
|
default_executable:
|
20
20
|
dependencies:
|
21
21
|
- !ruby/object:Gem::Dependency
|